From ac037d05e1674db20a8dfcf5644d1bf4ad8e5f2c Mon Sep 17 00:00:00 2001 From: Stephane M B079 Date: Tue, 18 Feb 2025 11:57:40 +0100 Subject: [PATCH] =?UTF-8?q?Ansible:=20plusieurs=20=C3=A9l=C3=A9ments=20int?= =?UTF-8?q?=C3=A9ressants?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__pycache__/__init__.cpython-311.pyc | Bin 11156 -> 11156 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 389 -> 389 bytes .../__pycache__/constants.cpython-311.pyc | Bin 11038 -> 11038 bytes .../__pycache__/context.cpython-311.pyc | Bin 2325 -> 2325 bytes .../__pycache__/release.cpython-311.pyc | Bin 349 -> 349 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 2137 -> 2137 bytes .../cli/__pycache__/__init__.cpython-311.pyc | Bin 33039 -> 33039 bytes .../cli/__pycache__/playbook.cpython-311.pyc | Bin 12243 -> 12243 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 259 -> 259 bytes .../option_helpers.cpython-311.pyc | Bin 27917 -> 27917 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 195 -> 195 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 190 -> 190 bytes .../__pycache__/manager.cpython-311.pyc | Bin 33842 -> 33842 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 18792 -> 18792 bytes .../__pycache__/yaml_strings.cpython-311.pyc | Bin 3209 -> 3209 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 254 -> 254 bytes .../action_write_locks.cpython-311.pyc | Bin 1274 -> 1274 bytes .../interpreter_discovery.cpython-311.pyc | Bin 12507 -> 12507 bytes .../__pycache__/module_common.cpython-311.pyc | Bin 63960 -> 63960 bytes .../__pycache__/play_iterator.cpython-311.pyc | Bin 31134 -> 31134 bytes .../playbook_executor.cpython-311.pyc | Bin 14766 -> 14766 bytes .../__pycache__/stats.cpython-311.pyc | Bin 4132 -> 4132 bytes .../__pycache__/task_executor.cpython-311.pyc | Bin 58858 -> 58858 bytes .../task_queue_manager.cpython-311.pyc | Bin 21862 -> 21862 bytes .../__pycache__/task_result.cpython-311.pyc | Bin 7412 -> 7412 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 202 -> 202 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 203 -> 203 bytes .../module_manifest.cpython-311.pyc | Bin 17490 -> 17490 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 262 -> 262 bytes .../__pycache__/worker.cpython-311.pyc | Bin 11732 -> 11732 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 193 -> 193 bytes .../__pycache__/data.cpython-311.pyc | Bin 12902 -> 12902 bytes .../__pycache__/group.cpython-311.pyc | Bin 14275 -> 14275 bytes .../__pycache__/helpers.cpython-311.pyc | Bin 1185 -> 1185 bytes .../__pycache__/host.cpython-311.pyc | Bin 7982 -> 7982 bytes .../__pycache__/manager.cpython-311.pyc | Bin 36588 -> 36588 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 196 -> 196 bytes .../__pycache__/_text.cpython-311.pyc | Bin 639 -> 639 bytes .../__pycache__/basic.cpython-311.pyc | Bin 101054 -> 101054 bytes .../__pycache__/connection.cpython-311.pyc | Bin 10710 -> 10710 bytes .../__pycache__/errors.cpython-311.pyc | Bin 7885 -> 7885 bytes .../__pycache__/json_utils.cpython-311.pyc | Bin 2289 -> 2289 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 206 -> 206 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 218 -> 218 bytes .../_daemon_threading.cpython-311.pyc | Bin 1975 -> 1975 bytes .../__pycache__/_futures.cpython-311.pyc | Bin 1457 -> 1457 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 203 -> 203 bytes .../common/__pycache__/_utils.cpython-311.pyc | Bin 1657 -> 1657 bytes .../__pycache__/arg_spec.cpython-311.pyc | Bin 16163 -> 16163 bytes .../__pycache__/collections.cpython-311.pyc | Bin 6560 -> 6560 bytes .../common/__pycache__/file.cpython-311.pyc | Bin 3733 -> 3733 bytes .../common/__pycache__/json.cpython-311.pyc | Bin 5152 -> 5152 bytes .../common/__pycache__/locale.cpython-311.pyc | Bin 2527 -> 2527 bytes .../__pycache__/parameters.cpython-311.pyc | Bin 40798 -> 40798 bytes .../__pycache__/process.cpython-311.pyc | Bin 2859 -> 2859 bytes .../__pycache__/sys_info.cpython-311.pyc | Bin 5193 -> 5193 bytes .../__pycache__/validation.cpython-311.pyc | Bin 24494 -> 24494 bytes .../__pycache__/warnings.cpython-311.pyc | Bin 1941 -> 1941 bytes .../common/__pycache__/yaml.cpython-311.pyc | Bin 1530 -> 1530 bytes .../text/__pycache__/__init__.cpython-311.pyc | Bin 208 -> 208 bytes .../__pycache__/converters.cpython-311.pyc | Bin 14323 -> 14323 bytes .../__pycache__/formatters.cpython-311.pyc | Bin 5967 -> 5967 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 203 -> 203 bytes .../__pycache__/datetime.cpython-311.pyc | Bin 1778 -> 1778 bytes .../__pycache__/paramiko.cpython-311.pyc | Bin 1005 -> 1005 bytes .../__pycache__/selinux.cpython-311.pyc | Bin 6309 -> 6309 bytes .../compat/__pycache__/typing.cpython-311.pyc | Bin 864 -> 864 bytes .../__pycache__/version.cpython-311.pyc | Bin 10610 -> 10610 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 933 -> 933 bytes .../__pycache__/_distro.cpython-311.pyc | Bin 56654 -> 56654 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 384 -> 384 bytes .../ansible_collector.cpython-311.pyc | Bin 5857 -> 5857 bytes .../__pycache__/collector.cpython-311.pyc | Bin 14075 -> 14075 bytes .../facts/__pycache__/compat.cpython-311.pyc | Bin 2639 -> 2639 bytes .../default_collectors.cpython-311.pyc | Bin 6758 -> 6758 bytes .../__pycache__/namespace.cpython-311.pyc | Bin 1978 -> 1978 bytes .../facts/__pycache__/sysctl.cpython-311.pyc | Bin 2116 -> 2116 bytes .../facts/__pycache__/timeout.cpython-311.pyc | Bin 2373 -> 2373 bytes .../facts/__pycache__/utils.cpython-311.pyc | Bin 3585 -> 3585 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 211 -> 211 bytes .../hardware/__pycache__/aix.cpython-311.pyc | Bin 12543 -> 12543 bytes .../hardware/__pycache__/base.cpython-311.pyc | Bin 1891 -> 1891 bytes .../__pycache__/darwin.cpython-311.pyc | Bin 7490 -> 7490 bytes .../__pycache__/dragonfly.cpython-311.pyc | Bin 776 -> 776 bytes .../__pycache__/freebsd.cpython-311.pyc | Bin 12724 -> 12724 bytes .../hardware/__pycache__/hpux.cpython-311.pyc | Bin 10630 -> 10630 bytes .../hardware/__pycache__/hurd.cpython-311.pyc | Bin 2043 -> 2043 bytes .../__pycache__/linux.cpython-311.pyc | Bin 41729 -> 41729 bytes .../__pycache__/netbsd.cpython-311.pyc | Bin 7898 -> 7898 bytes .../__pycache__/openbsd.cpython-311.pyc | Bin 7898 -> 7898 bytes .../__pycache__/sunos.cpython-311.pyc | Bin 12196 -> 12196 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 210 -> 210 bytes .../network/__pycache__/aix.cpython-311.pyc | Bin 6688 -> 6688 bytes .../network/__pycache__/base.cpython-311.pyc | Bin 2393 -> 2393 bytes .../__pycache__/darwin.cpython-311.pyc | Bin 1912 -> 1912 bytes .../__pycache__/dragonfly.cpython-311.pyc | Bin 1147 -> 1147 bytes .../__pycache__/fc_wwn.cpython-311.pyc | Bin 4820 -> 4820 bytes .../__pycache__/freebsd.cpython-311.pyc | Bin 1137 -> 1137 bytes .../__pycache__/generic_bsd.cpython-311.pyc | Bin 14764 -> 14764 bytes .../network/__pycache__/hpux.cpython-311.pyc | Bin 3929 -> 3929 bytes .../network/__pycache__/hurd.cpython-311.pyc | Bin 3242 -> 3242 bytes .../network/__pycache__/iscsi.cpython-311.pyc | Bin 5393 -> 5393 bytes .../network/__pycache__/linux.cpython-311.pyc | Bin 20454 -> 20454 bytes .../__pycache__/netbsd.cpython-311.pyc | Bin 1649 -> 1649 bytes .../network/__pycache__/nvme.cpython-311.pyc | Bin 2186 -> 2186 bytes .../__pycache__/openbsd.cpython-311.pyc | Bin 1811 -> 1811 bytes .../network/__pycache__/sunos.cpython-311.pyc | Bin 5093 -> 5093 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 208 -> 208 bytes .../other/__pycache__/facter.cpython-311.pyc | Bin 3291 -> 3291 bytes .../other/__pycache__/ohai.cpython-311.pyc | Bin 2977 -> 2977 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 209 -> 209 bytes .../__pycache__/apparmor.cpython-311.pyc | Bin 1297 -> 1297 bytes .../system/__pycache__/caps.cpython-311.pyc | Bin 2776 -> 2776 bytes .../system/__pycache__/chroot.cpython-311.pyc | Bin 2138 -> 2138 bytes .../__pycache__/cmdline.cpython-311.pyc | Bin 3278 -> 3278 bytes .../__pycache__/date_time.cpython-311.pyc | Bin 3718 -> 3718 bytes .../__pycache__/distribution.cpython-311.pyc | Bin 34329 -> 34329 bytes .../system/__pycache__/dns.cpython-311.pyc | Bin 3129 -> 3129 bytes .../system/__pycache__/env.cpython-311.pyc | Bin 1275 -> 1275 bytes .../system/__pycache__/fips.cpython-311.pyc | Bin 1258 -> 1258 bytes .../__pycache__/loadavg.cpython-311.pyc | Bin 1331 -> 1331 bytes .../system/__pycache__/local.cpython-311.pyc | Bin 5173 -> 5173 bytes .../system/__pycache__/lsb.cpython-311.pyc | Bin 3748 -> 3748 bytes .../__pycache__/pkg_mgr.cpython-311.pyc | Bin 5980 -> 5980 bytes .../__pycache__/platform.cpython-311.pyc | Bin 4126 -> 4126 bytes .../system/__pycache__/python.cpython-311.pyc | Bin 2239 -> 2239 bytes .../__pycache__/selinux.cpython-311.pyc | Bin 2966 -> 2966 bytes .../__pycache__/service_mgr.cpython-311.pyc | Bin 5755 -> 5755 bytes .../__pycache__/ssh_pub_keys.cpython-311.pyc | Bin 1896 -> 1896 bytes .../__pycache__/systemd.cpython-311.pyc | Bin 1822 -> 1822 bytes .../system/__pycache__/user.cpython-311.pyc | Bin 2172 -> 2172 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 210 -> 210 bytes .../virtual/__pycache__/base.cpython-311.pyc | Bin 2615 -> 2615 bytes .../__pycache__/dragonfly.cpython-311.pyc | Bin 707 -> 707 bytes .../__pycache__/freebsd.cpython-311.pyc | Bin 3240 -> 3240 bytes .../virtual/__pycache__/hpux.cpython-311.pyc | Bin 3124 -> 3124 bytes .../virtual/__pycache__/linux.cpython-311.pyc | Bin 13953 -> 13953 bytes .../__pycache__/netbsd.cpython-311.pyc | Bin 2926 -> 2926 bytes .../__pycache__/openbsd.cpython-311.pyc | Bin 2984 -> 2984 bytes .../virtual/__pycache__/sunos.cpython-311.pyc | Bin 5283 -> 5283 bytes .../__pycache__/sysctl.cpython-311.pyc | Bin 4435 -> 4435 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 204 -> 204 bytes .../__pycache__/convert_bool.cpython-311.pyc | Bin 1977 -> 1977 bytes .../six/__pycache__/__init__.cpython-311.pyc | Bin 46500 -> 46500 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 191 -> 191 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 253 -> 253 bytes .../parsing/__pycache__/ajson.cpython-311.pyc | Bin 2101 -> 2101 bytes .../__pycache__/dataloader.cpython-311.pyc | Bin 27933 -> 27933 bytes .../__pycache__/mod_args.cpython-311.pyc | Bin 14199 -> 14199 bytes .../__pycache__/plugin_docs.cpython-311.pyc | Bin 10603 -> 10603 bytes .../__pycache__/quoting.cpython-311.pyc | Bin 892 -> 892 bytes .../__pycache__/splitter.cpython-311.pyc | Bin 8247 -> 8247 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 259 -> 259 bytes .../__pycache__/addresses.cpython-311.pyc | Bin 5910 -> 5910 bytes .../utils/__pycache__/jsonify.cpython-311.pyc | Bin 854 -> 854 bytes .../utils/__pycache__/yaml.cpython-311.pyc | Bin 3724 -> 3724 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 62879 -> 62879 bytes .../yaml/__pycache__/__init__.cpython-311.pyc | Bin 258 -> 258 bytes .../__pycache__/constructor.cpython-311.pyc | Bin 7825 -> 7825 bytes .../yaml/__pycache__/dumper.cpython-311.pyc | Bin 4133 -> 4133 bytes .../yaml/__pycache__/loader.cpython-311.pyc | Bin 2116 -> 2116 bytes .../yaml/__pycache__/objects.cpython-311.pyc | Bin 21490 -> 21490 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 6159 -> 6159 bytes .../__pycache__/attribute.cpython-311.pyc | Bin 8926 -> 8926 bytes .../playbook/__pycache__/base.cpython-311.pyc | Bin 38889 -> 38889 bytes .../__pycache__/block.cpython-311.pyc | Bin 19504 -> 19504 bytes .../collectionsearch.cpython-311.pyc | Bin 2510 -> 2510 bytes .../__pycache__/conditional.cpython-311.pyc | Bin 5342 -> 5342 bytes .../__pycache__/delegatable.cpython-311.pyc | Bin 1081 -> 1081 bytes .../__pycache__/handler.cpython-311.pyc | Bin 4116 -> 4116 bytes .../handler_task_include.cpython-311.pyc | Bin 1300 -> 1300 bytes .../__pycache__/helpers.cpython-311.pyc | Bin 12608 -> 12608 bytes .../__pycache__/included_file.cpython-311.pyc | Bin 9919 -> 9919 bytes .../__pycache__/loop_control.cpython-311.pyc | Bin 2235 -> 2235 bytes .../__pycache__/notifiable.cpython-311.pyc | Bin 631 -> 631 bytes .../playbook/__pycache__/play.cpython-311.pyc | Bin 18960 -> 18960 bytes .../__pycache__/play_context.cpython-311.pyc | Bin 11940 -> 11940 bytes .../playbook_include.cpython-311.pyc | Bin 8280 -> 8280 bytes .../__pycache__/role_include.cpython-311.pyc | Bin 8669 -> 8669 bytes .../__pycache__/taggable.cpython-311.pyc | Bin 3593 -> 3593 bytes .../playbook/__pycache__/task.cpython-311.pyc | Bin 23580 -> 23580 bytes .../__pycache__/task_include.cpython-311.pyc | Bin 6526 -> 6526 bytes .../role/__pycache__/__init__.cpython-311.pyc | Bin 31032 -> 31032 bytes .../__pycache__/definition.cpython-311.pyc | Bin 9363 -> 9363 bytes .../role/__pycache__/include.cpython-311.pyc | Bin 2631 -> 2631 bytes .../role/__pycache__/metadata.cpython-311.pyc | Bin 6000 -> 6000 bytes .../__pycache__/requirement.cpython-311.pyc | Bin 4731 -> 4731 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 8671 -> 8671 bytes .../__pycache__/loader.cpython-311.pyc | Bin 79129 -> 79129 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 64468 -> 64468 bytes .../__pycache__/command.cpython-311.pyc | Bin 1531 -> 1531 bytes .../action/__pycache__/copy.cpython-311.pyc | Bin 25541 -> 25541 bytes .../action/__pycache__/debug.cpython-311.pyc | Bin 3082 -> 3082 bytes .../__pycache__/gather_facts.cpython-311.pyc | Bin 10545 -> 10545 bytes .../action/__pycache__/normal.cpython-311.pyc | Bin 1696 -> 1696 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 6950 -> 6950 bytes .../become/__pycache__/runas.cpython-311.pyc | Bin 2865 -> 2865 bytes .../become/__pycache__/su.cpython-311.pyc | Bin 6229 -> 6229 bytes .../become/__pycache__/sudo.cpython-311.pyc | Bin 4666 -> 4666 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 21799 -> 21799 bytes .../cache/__pycache__/memory.cpython-311.pyc | Bin 2680 -> 2680 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 33367 -> 33367 bytes .../__pycache__/default.cpython-311.pyc | Bin 26863 -> 26863 bytes .../__pycache__/junit.cpython-311.pyc | Bin 20174 -> 20174 bytes .../__pycache__/minimal.cpython-311.pyc | Bin 5791 -> 5791 bytes .../__pycache__/oneline.cpython-311.pyc | Bin 6105 -> 6105 bytes .../callback/__pycache__/tree.cpython-311.pyc | Bin 5305 -> 5305 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 24437 -> 24437 bytes .../__pycache__/local.cpython-311.pyc | Bin 12829 -> 12829 bytes .../__pycache__/paramiko_ssh.cpython-311.pyc | Bin 33866 -> 33866 bytes .../__pycache__/psrp.cpython-311.pyc | Bin 37704 -> 37704 bytes .../__pycache__/ssh.cpython-311.pyc | Bin 62269 -> 62269 bytes .../__pycache__/winrm.cpython-311.pyc | Bin 49252 -> 49252 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 205 -> 205 bytes .../connection_pipelining.cpython-311.pyc | Bin 1469 -> 1469 bytes .../default_callback.cpython-311.pyc | Bin 3518 -> 3518 bytes .../result_format_callback.cpython-311.pyc | Bin 2247 -> 2247 bytes .../__pycache__/shell_common.cpython-311.pyc | Bin 3571 -> 3571 bytes .../__pycache__/shell_windows.cpython-311.pyc | Bin 1355 -> 1355 bytes .../vars_plugin_staging.cpython-311.pyc | Bin 1192 -> 1192 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 969 -> 969 bytes .../filter/__pycache__/core.cpython-311.pyc | Bin 30801 -> 30801 bytes .../__pycache__/encryption.cpython-311.pyc | Bin 5040 -> 5040 bytes .../__pycache__/mathstuff.cpython-311.pyc | Bin 11767 -> 11767 bytes .../filter/__pycache__/urls.cpython-311.pyc | Bin 887 -> 887 bytes .../__pycache__/urlsplit.cpython-311.pyc | Bin 3062 -> 3062 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 26090 -> 26090 bytes .../__pycache__/auto.cpython-311.pyc | Bin 3761 -> 3761 bytes .../__pycache__/host_list.cpython-311.pyc | Bin 3506 -> 3506 bytes .../inventory/__pycache__/ini.cpython-311.pyc | Bin 17473 -> 17473 bytes .../__pycache__/script.cpython-311.pyc | Bin 15323 -> 15323 bytes .../__pycache__/toml.cpython-311.pyc | Bin 12973 -> 12973 bytes .../__pycache__/yaml.cpython-311.pyc | Bin 9423 -> 9423 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 11738 -> 11738 bytes .../shell/__pycache__/cmd.cpython-311.pyc | Bin 1599 -> 1599 bytes .../__pycache__/powershell.cpython-311.pyc | Bin 18509 -> 18509 bytes .../shell/__pycache__/sh.cpython-311.pyc | Bin 2954 -> 2954 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 63100 -> 63100 bytes .../__pycache__/linear.cpython-311.pyc | Bin 18436 -> 18436 bytes .../test/__pycache__/__init__.cpython-311.pyc | Bin 901 -> 901 bytes .../test/__pycache__/core.cpython-311.pyc | Bin 12428 -> 12428 bytes .../test/__pycache__/files.cpython-311.pyc | Bin 1282 -> 1282 bytes .../__pycache__/mathstuff.cpython-311.pyc | Bin 1783 -> 1783 bytes .../test/__pycache__/uri.cpython-311.pyc | Bin 2153 -> 2153 bytes .../vars/__pycache__/__init__.cpython-311.pyc | Bin 1476 -> 1476 bytes .../host_group_vars.cpython-311.pyc | Bin 6487 -> 6487 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 46038 -> 46038 bytes .../native_helpers.cpython-311.pyc | Bin 9285 -> 9285 bytes .../__pycache__/template.cpython-311.pyc | Bin 1638 -> 1638 bytes .../template/__pycache__/vars.cpython-311.pyc | Bin 4023 -> 4023 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 251 -> 251 bytes .../__pycache__/_junit_xml.cpython-311.pyc | Bin 18127 -> 18127 bytes .../utils/__pycache__/color.cpython-311.pyc | Bin 3982 -> 3982 bytes .../context_objects.cpython-311.pyc | Bin 4815 -> 4815 bytes .../utils/__pycache__/display.cpython-311.pyc | Bin 42056 -> 42056 bytes .../utils/__pycache__/encrypt.cpython-311.pyc | Bin 9275 -> 9275 bytes .../utils/__pycache__/fqcn.cpython-311.pyc | Bin 920 -> 920 bytes .../utils/__pycache__/galaxy.cpython-311.pyc | Bin 5428 -> 5428 bytes .../utils/__pycache__/hashing.cpython-311.pyc | Bin 3008 -> 3008 bytes .../utils/__pycache__/helpers.cpython-311.pyc | Bin 2399 -> 2399 bytes .../utils/__pycache__/listify.cpython-311.pyc | Bin 1173 -> 1173 bytes .../utils/__pycache__/lock.cpython-311.pyc | Bin 1876 -> 1876 bytes .../multiprocessing.cpython-311.pyc | Bin 373 -> 373 bytes .../__pycache__/native_jinja.cpython-311.pyc | Bin 567 -> 567 bytes .../utils/__pycache__/path.cpython-311.pyc | Bin 7987 -> 7987 bytes .../__pycache__/plugin_docs.cpython-311.pyc | Bin 18392 -> 18392 bytes .../__pycache__/sentinel.cpython-311.pyc | Bin 2768 -> 2768 bytes .../utils/__pycache__/shlex.cpython-311.pyc | Bin 313 -> 313 bytes .../__pycache__/singleton.cpython-311.pyc | Bin 1804 -> 1804 bytes .../__pycache__/ssh_functions.cpython-311.pyc | Bin 2188 -> 2188 bytes .../utils/__pycache__/unicode.cpython-311.pyc | Bin 758 -> 758 bytes .../__pycache__/unsafe_proxy.cpython-311.pyc | Bin 29146 -> 29146 bytes .../utils/__pycache__/vars.cpython-311.pyc | Bin 9099 -> 9099 bytes .../utils/__pycache__/version.cpython-311.pyc | Bin 12855 -> 12855 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 1142 -> 1142 bytes .../_collection_config.cpython-311.pyc | Bin 6409 -> 6409 bytes .../_collection_finder.cpython-311.pyc | Bin 66539 -> 66539 bytes .../_collection_meta.cpython-311.pyc | Bin 1416 -> 1416 bytes .../vars/__pycache__/__init__.cpython-311.pyc | Bin 188 -> 188 bytes .../vars/__pycache__/clean.cpython-311.pyc | Bin 7925 -> 7925 bytes .../__pycache__/fact_cache.cpython-311.pyc | Bin 4280 -> 4280 bytes .../vars/__pycache__/hostvars.cpython-311.pyc | Bin 7669 -> 7669 bytes .../vars/__pycache__/manager.cpython-311.pyc | Bin 35934 -> 35934 bytes .../vars/__pycache__/plugins.cpython-311.pyc | Bin 5429 -> 5429 bytes .../vars/__pycache__/reserved.cpython-311.pyc | Bin 3242 -> 3242 bytes .../__pycache__/__about__.cpython-311.pyc | Bin 480 -> 480 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 954 -> 954 bytes .../__pycache__/exceptions.cpython-311.pyc | Bin 2957 -> 2957 bytes .../__pycache__/utils.cpython-311.pyc | Bin 7107 -> 7107 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 261 -> 261 bytes .../hazmat/__pycache__/_oid.cpython-311.pyc | Bin 18979 -> 18979 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 571 -> 571 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 391 -> 391 bytes .../__pycache__/backend.cpython-311.pyc | Bin 13359 -> 13359 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 204 -> 204 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 212 -> 212 bytes .../__pycache__/_conditional.cpython-311.pyc | Bin 5528 -> 5528 bytes .../__pycache__/binding.cpython-311.pyc | Bin 5602 -> 5602 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 266 -> 266 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 274 -> 274 bytes .../__pycache__/algorithms.cpython-311.pyc | Bin 5670 -> 5670 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 206 -> 206 bytes .../__pycache__/_asymmetric.cpython-311.pyc | Bin 912 -> 912 bytes .../_cipheralgorithm.cpython-311.pyc | Bin 2741 -> 2741 bytes .../_serialization.cpython-311.pyc | Bin 7708 -> 7708 bytes .../__pycache__/constant_time.cpython-311.pyc | Bin 778 -> 778 bytes .../__pycache__/hashes.cpython-311.pyc | Bin 9486 -> 9486 bytes .../__pycache__/hmac.cpython-311.pyc | Bin 609 -> 609 bytes .../__pycache__/padding.cpython-311.pyc | Bin 8504 -> 8504 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 217 -> 217 bytes .../asymmetric/__pycache__/ec.cpython-311.pyc | Bin 17566 -> 17566 bytes .../__pycache__/padding.cpython-311.pyc | Bin 5593 -> 5593 bytes .../__pycache__/rsa.cpython-311.pyc | Bin 10226 -> 10226 bytes .../__pycache__/utils.cpython-311.pyc | Bin 1458 -> 1458 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 759 -> 759 bytes .../__pycache__/algorithms.cpython-311.pyc | Bin 6973 -> 6973 bytes .../ciphers/__pycache__/base.cpython-311.pyc | Bin 7393 -> 7393 bytes .../ciphers/__pycache__/modes.cpython-311.pyc | Bin 13231 -> 13231 bytes .../kdf/__pycache__/__init__.cpython-311.pyc | Bin 1368 -> 1368 bytes .../kdf/__pycache__/pbkdf2.cpython-311.pyc | Bin 2994 -> 2994 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 2115 -> 2115 bytes .../__pycache__/_identifier.cpython-311.pyc | Bin 2131 -> 2131 bytes .../__pycache__/async_utils.cpython-311.pyc | Bin 5591 -> 5591 bytes .../__pycache__/bccache.cpython-311.pyc | Bin 20913 -> 20913 bytes .../__pycache__/compiler.cpython-311.pyc | Bin 112435 -> 112435 bytes .../__pycache__/defaults.cpython-311.pyc | Bin 1716 -> 1716 bytes .../__pycache__/environment.cpython-311.pyc | Bin 80573 -> 80573 bytes .../__pycache__/exceptions.cpython-311.pyc | Bin 8601 -> 8601 bytes .../__pycache__/filters.cpython-311.pyc | Bin 77853 -> 77853 bytes .../__pycache__/idtracking.cpython-311.pyc | Bin 19505 -> 19505 bytes .../jinja2/__pycache__/lexer.cpython-311.pyc | Bin 35658 -> 35658 bytes .../__pycache__/loaders.cpython-311.pyc | Bin 34391 -> 34391 bytes .../__pycache__/nativetypes.cpython-311.pyc | Bin 7957 -> 7957 bytes .../jinja2/__pycache__/nodes.cpython-311.pyc | Bin 64476 -> 64476 bytes .../__pycache__/optimizer.cpython-311.pyc | Bin 2846 -> 2846 bytes .../jinja2/__pycache__/parser.cpython-311.pyc | Bin 60022 -> 60022 bytes .../__pycache__/runtime.cpython-311.pyc | Bin 51109 -> 51109 bytes .../jinja2/__pycache__/tests.cpython-311.pyc | Bin 9266 -> 9266 bytes .../jinja2/__pycache__/utils.cpython-311.pyc | Bin 37553 -> 37553 bytes .../__pycache__/visitor.cpython-311.pyc | Bin 5694 -> 5694 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 25732 -> 25732 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 545 -> 545 bytes .../__pycache__/_elffile.cpython-311.pyc | Bin 5503 -> 5503 bytes .../__pycache__/_manylinux.cpython-311.pyc | Bin 10936 -> 10936 bytes .../__pycache__/_musllinux.cpython-311.pyc | Bin 5296 -> 5296 bytes .../__pycache__/_structures.cpython-311.pyc | Bin 3669 -> 3669 bytes .../__pycache__/specifiers.cpython-311.pyc | Bin 41497 -> 41497 bytes .../__pycache__/tags.cpython-311.pyc | Bin 25869 -> 25869 bytes .../__pycache__/utils.cpython-311.pyc | Bin 7571 -> 7571 bytes .../__pycache__/version.cpython-311.pyc | Bin 21958 -> 21958 bytes .../passlib-1.7.4.dist-info/INSTALLER | 1 + .../passlib-1.7.4.dist-info/LICENSE | 116 + .../passlib-1.7.4.dist-info/METADATA | 40 + .../passlib-1.7.4.dist-info/RECORD | 202 + .../passlib-1.7.4.dist-info/REQUESTED | 0 .../passlib-1.7.4.dist-info/WHEEL | 6 + .../passlib-1.7.4.dist-info/top_level.txt | 1 + .../passlib-1.7.4.dist-info/zip-safe | 1 + .../site-packages/passlib/__init__.py | 3 + .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 281 bytes .../__pycache__/apache.cpython-311.pyc | Bin 0 -> 46416 bytes .../passlib/__pycache__/apps.cpython-311.pyc | Bin 0 -> 4936 bytes .../__pycache__/context.cpython-311.pyc | Bin 0 -> 104415 bytes .../passlib/__pycache__/exc.cpython-311.pyc | Bin 0 -> 17259 bytes .../passlib/__pycache__/hash.cpython-311.pyc | Bin 0 -> 2480 bytes .../passlib/__pycache__/hosts.cpython-311.pyc | Bin 0 -> 1657 bytes .../passlib/__pycache__/ifc.cpython-311.pyc | Bin 0 -> 8306 bytes .../passlib/__pycache__/pwd.cpython-311.pyc | Bin 0 -> 28567 bytes .../__pycache__/registry.cpython-311.pyc | Bin 0 -> 20697 bytes .../passlib/__pycache__/totp.cpython-311.pyc | Bin 0 -> 71041 bytes .../passlib/__pycache__/win32.cpython-311.pyc | Bin 0 -> 2539 bytes .../passlib/_data/wordsets/bip39.txt | 2049 +++++ .../passlib/_data/wordsets/eff_long.txt | 7776 +++++++++++++++++ .../passlib/_data/wordsets/eff_prefixed.txt | 1296 +++ .../passlib/_data/wordsets/eff_short.txt | 1296 +++ .../site-packages/passlib/apache.py | 1255 +++ .../python3.11/site-packages/passlib/apps.py | 245 + .../site-packages/passlib/context.py | 2637 ++++++ .../site-packages/passlib/crypto/__init__.py | 1 + .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 281 bytes .../crypto/__pycache__/_md4.cpython-311.pyc | Bin 0 -> 8529 bytes .../crypto/__pycache__/des.cpython-311.pyc | Bin 0 -> 25996 bytes .../crypto/__pycache__/digest.cpython-311.pyc | Bin 0 -> 33156 bytes .../passlib/crypto/_blowfish/__init__.py | 169 + .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 5878 bytes .../__pycache__/_gen_files.cpython-311.pyc | Bin 0 -> 8396 bytes .../__pycache__/base.cpython-311.pyc | Bin 0 -> 14769 bytes .../__pycache__/unrolled.cpython-311.pyc | Bin 0 -> 41718 bytes .../passlib/crypto/_blowfish/_gen_files.py | 204 + .../passlib/crypto/_blowfish/base.py | 441 + .../passlib/crypto/_blowfish/unrolled.py | 771 ++ .../site-packages/passlib/crypto/_md4.py | 244 + .../site-packages/passlib/crypto/des.py | 848 ++ .../site-packages/passlib/crypto/digest.py | 1057 +++ .../passlib/crypto/scrypt/__init__.py | 281 + .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 8671 bytes .../__pycache__/_builtin.cpython-311.pyc | Bin 0 -> 8862 bytes .../__pycache__/_gen_files.cpython-311.pyc | Bin 0 -> 4090 bytes .../scrypt/__pycache__/_salsa.cpython-311.pyc | Bin 0 -> 4712 bytes .../passlib/crypto/scrypt/_builtin.py | 244 + .../passlib/crypto/scrypt/_gen_files.py | 154 + .../passlib/crypto/scrypt/_salsa.py | 170 + .../python3.11/site-packages/passlib/exc.py | 397 + .../site-packages/passlib/ext/__init__.py | 1 + .../ext/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 187 bytes .../passlib/ext/django/__init__.py | 6 + .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 437 bytes .../django/__pycache__/models.cpython-311.pyc | Bin 0 -> 683 bytes .../django/__pycache__/utils.cpython-311.pyc | Bin 0 -> 42921 bytes .../passlib/ext/django/models.py | 36 + .../site-packages/passlib/ext/django/utils.py | 1276 +++ .../passlib/handlers/__init__.py | 1 + .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 285 bytes .../__pycache__/argon2.cpython-311.pyc | Bin 0 -> 32317 bytes .../__pycache__/bcrypt.cpython-311.pyc | Bin 0 -> 43326 bytes .../__pycache__/cisco.cpython-311.pyc | Bin 0 -> 14185 bytes .../__pycache__/des_crypt.cpython-311.pyc | Bin 0 -> 22513 bytes .../__pycache__/digests.cpython-311.pyc | Bin 0 -> 7033 bytes .../__pycache__/django.cpython-311.pyc | Bin 0 -> 22705 bytes .../handlers/__pycache__/fshp.cpython-311.pyc | Bin 0 -> 8645 bytes .../__pycache__/ldap_digests.cpython-311.pyc | Bin 0 -> 15489 bytes .../__pycache__/md5_crypt.cpython-311.pyc | Bin 0 -> 10144 bytes .../handlers/__pycache__/misc.cpython-311.pyc | Bin 0 -> 12483 bytes .../__pycache__/mssql.cpython-311.pyc | Bin 0 -> 10146 bytes .../__pycache__/mysql.cpython-311.pyc | Bin 0 -> 4810 bytes .../__pycache__/oracle.cpython-311.pyc | Bin 0 -> 7279 bytes .../__pycache__/pbkdf2.cpython-311.pyc | Bin 0 -> 18781 bytes .../__pycache__/phpass.cpython-311.pyc | Bin 0 -> 5287 bytes .../__pycache__/postgres.cpython-311.pyc | Bin 0 -> 2271 bytes .../__pycache__/roundup.cpython-311.pyc | Bin 0 -> 1028 bytes .../__pycache__/scram.cpython-311.pyc | Bin 0 -> 16881 bytes .../__pycache__/scrypt.cpython-311.pyc | Bin 0 -> 14032 bytes .../__pycache__/sha1_crypt.cpython-311.pyc | Bin 0 -> 6277 bytes .../__pycache__/sha2_crypt.cpython-311.pyc | Bin 0 -> 16710 bytes .../__pycache__/sun_md5_crypt.cpython-311.pyc | Bin 0 -> 13505 bytes .../__pycache__/windows.cpython-311.pyc | Bin 0 -> 12429 bytes .../site-packages/passlib/handlers/argon2.py | 1009 +++ .../site-packages/passlib/handlers/bcrypt.py | 1243 +++ .../site-packages/passlib/handlers/cisco.py | 440 + .../passlib/handlers/des_crypt.py | 607 ++ .../site-packages/passlib/handlers/digests.py | 168 + .../site-packages/passlib/handlers/django.py | 512 ++ .../site-packages/passlib/handlers/fshp.py | 214 + .../passlib/handlers/ldap_digests.py | 359 + .../passlib/handlers/md5_crypt.py | 346 + .../site-packages/passlib/handlers/misc.py | 269 + .../site-packages/passlib/handlers/mssql.py | 244 + .../site-packages/passlib/handlers/mysql.py | 128 + .../site-packages/passlib/handlers/oracle.py | 172 + .../site-packages/passlib/handlers/pbkdf2.py | 475 + .../site-packages/passlib/handlers/phpass.py | 135 + .../passlib/handlers/postgres.py | 55 + .../site-packages/passlib/handlers/roundup.py | 29 + .../site-packages/passlib/handlers/scram.py | 582 ++ .../site-packages/passlib/handlers/scrypt.py | 383 + .../passlib/handlers/sha1_crypt.py | 158 + .../passlib/handlers/sha2_crypt.py | 534 ++ .../passlib/handlers/sun_md5_crypt.py | 363 + .../site-packages/passlib/handlers/windows.py | 334 + .../python3.11/site-packages/passlib/hash.py | 68 + .../python3.11/site-packages/passlib/hosts.py | 106 + .../python3.11/site-packages/passlib/ifc.py | 353 + .../python3.11/site-packages/passlib/pwd.py | 809 ++ .../site-packages/passlib/registry.py | 547 ++ .../site-packages/passlib/tests/__init__.py | 1 + .../site-packages/passlib/tests/__main__.py | 6 + .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 216 bytes .../__pycache__/__main__.cpython-311.pyc | Bin 0 -> 414 bytes .../_test_bad_register.cpython-311.pyc | Bin 0 -> 1047 bytes .../__pycache__/backports.cpython-311.pyc | Bin 0 -> 1612 bytes .../__pycache__/test_apache.cpython-311.pyc | Bin 0 -> 45969 bytes .../__pycache__/test_apps.cpython-311.pyc | Bin 0 -> 7969 bytes .../__pycache__/test_context.cpython-311.pyc | Bin 0 -> 87495 bytes .../test_context_deprecated.cpython-311.pyc | Bin 0 -> 34743 bytes .../test_crypto_builtin_md4.cpython-311.pyc | Bin 0 -> 8270 bytes .../test_crypto_des.cpython-311.pyc | Bin 0 -> 8569 bytes .../test_crypto_digest.cpython-311.pyc | Bin 0 -> 21668 bytes .../test_crypto_scrypt.cpython-311.pyc | Bin 0 -> 30351 bytes .../test_ext_django.cpython-311.pyc | Bin 0 -> 43378 bytes .../test_ext_django_source.cpython-311.pyc | Bin 0 -> 9618 bytes .../__pycache__/test_handlers.cpython-311.pyc | Bin 0 -> 58973 bytes .../test_handlers_argon2.cpython-311.pyc | Bin 0 -> 22292 bytes .../test_handlers_bcrypt.cpython-311.pyc | Bin 0 -> 29991 bytes .../test_handlers_cisco.cpython-311.pyc | Bin 0 -> 11703 bytes .../test_handlers_django.cpython-311.pyc | Bin 0 -> 18338 bytes .../test_handlers_pbkdf2.cpython-311.pyc | Bin 0 -> 20889 bytes .../test_handlers_scrypt.cpython-311.pyc | Bin 0 -> 4159 bytes .../__pycache__/test_hosts.cpython-311.pyc | Bin 0 -> 4581 bytes .../__pycache__/test_pwd.cpython-311.pyc | Bin 0 -> 10393 bytes .../__pycache__/test_registry.cpython-311.pyc | Bin 0 -> 13437 bytes .../__pycache__/test_totp.cpython-311.pyc | Bin 0 -> 71972 bytes .../__pycache__/test_utils.cpython-311.pyc | Bin 0 -> 59979 bytes .../test_utils_handlers.cpython-311.pyc | Bin 0 -> 50766 bytes .../test_utils_md4.cpython-311.pyc | Bin 0 -> 1809 bytes .../test_utils_pbkdf2.cpython-311.pyc | Bin 0 -> 14088 bytes .../__pycache__/test_win32.cpython-311.pyc | Bin 0 -> 2584 bytes .../__pycache__/tox_support.cpython-311.pyc | Bin 0 -> 3869 bytes .../tests/__pycache__/utils.cpython-311.pyc | Bin 0 -> 159461 bytes .../passlib/tests/_test_bad_register.py | 15 + .../site-packages/passlib/tests/backports.py | 67 + .../site-packages/passlib/tests/sample1.cfg | 9 + .../site-packages/passlib/tests/sample1b.cfg | 9 + .../site-packages/passlib/tests/sample1c.cfg | Bin 0 -> 490 bytes .../passlib/tests/sample_config_1s.cfg | 8 + .../passlib/tests/test_apache.py | 769 ++ .../site-packages/passlib/tests/test_apps.py | 139 + .../passlib/tests/test_context.py | 1786 ++++ .../passlib/tests/test_context_deprecated.py | 743 ++ .../passlib/tests/test_crypto_builtin_md4.py | 160 + .../passlib/tests/test_crypto_des.py | 194 + .../passlib/tests/test_crypto_digest.py | 544 ++ .../passlib/tests/test_crypto_scrypt.py | 634 ++ .../passlib/tests/test_ext_django.py | 1080 +++ .../passlib/tests/test_ext_django_source.py | 250 + .../passlib/tests/test_handlers.py | 1819 ++++ .../passlib/tests/test_handlers_argon2.py | 507 ++ .../passlib/tests/test_handlers_bcrypt.py | 688 ++ .../passlib/tests/test_handlers_cisco.py | 457 + .../passlib/tests/test_handlers_django.py | 413 + .../passlib/tests/test_handlers_pbkdf2.py | 480 + .../passlib/tests/test_handlers_scrypt.py | 111 + .../site-packages/passlib/tests/test_hosts.py | 97 + .../site-packages/passlib/tests/test_pwd.py | 205 + .../passlib/tests/test_registry.py | 228 + .../site-packages/passlib/tests/test_totp.py | 1604 ++++ .../site-packages/passlib/tests/test_utils.py | 1171 +++ .../passlib/tests/test_utils_handlers.py | 870 ++ .../passlib/tests/test_utils_md4.py | 41 + .../passlib/tests/test_utils_pbkdf2.py | 323 + .../site-packages/passlib/tests/test_win32.py | 50 + .../passlib/tests/tox_support.py | 83 + .../site-packages/passlib/tests/utils.py | 3621 ++++++++ .../python3.11/site-packages/passlib/totp.py | 1908 ++++ .../site-packages/passlib/utils/__init__.py | 1220 +++ .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 43859 bytes .../utils/__pycache__/binary.cpython-311.pyc | Bin 0 -> 33081 bytes .../utils/__pycache__/decor.cpython-311.pyc | Bin 0 -> 9570 bytes .../utils/__pycache__/des.cpython-311.pyc | Bin 0 -> 2028 bytes .../__pycache__/handlers.cpython-311.pyc | Bin 0 -> 100192 bytes .../utils/__pycache__/md4.cpython-311.pyc | Bin 0 -> 897 bytes .../utils/__pycache__/pbkdf2.cpython-311.pyc | Bin 0 -> 6779 bytes .../site-packages/passlib/utils/binary.py | 884 ++ .../passlib/utils/compat/__init__.py | 474 + .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 16081 bytes .../__pycache__/_ordered_dict.cpython-311.pyc | Bin 0 -> 11816 bytes .../passlib/utils/compat/_ordered_dict.py | 242 + .../site-packages/passlib/utils/decor.py | 233 + .../site-packages/passlib/utils/des.py | 46 + .../site-packages/passlib/utils/handlers.py | 2711 ++++++ .../site-packages/passlib/utils/md4.py | 29 + .../site-packages/passlib/utils/pbkdf2.py | 193 + .../python3.11/site-packages/passlib/win32.py | 68 + .../pip/__pycache__/__init__.cpython-311.pyc | Bin 768 -> 768 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 951 -> 951 bytes .../__pycache__/build_env.cpython-311.pyc | Bin 16071 -> 16071 bytes .../__pycache__/cache.cpython-311.pyc | Bin 14696 -> 14696 bytes .../__pycache__/configuration.cpython-311.pyc | Bin 19227 -> 19227 bytes .../__pycache__/exceptions.cpython-311.pyc | Bin 38356 -> 38356 bytes .../__pycache__/pyproject.cpython-311.pyc | Bin 5519 -> 5519 bytes .../self_outdated_check.cpython-311.pyc | Bin 11321 -> 11321 bytes .../__pycache__/wheel_builder.cpython-311.pyc | Bin 15991 -> 15991 bytes .../cli/__pycache__/__init__.cpython-311.pyc | Bin 286 -> 286 bytes .../autocompletion.cpython-311.pyc | Bin 10076 -> 10076 bytes .../__pycache__/base_command.cpython-311.pyc | Bin 11074 -> 11074 bytes .../__pycache__/cmdoptions.cpython-311.pyc | Bin 32973 -> 32973 bytes .../command_context.cpython-311.pyc | Bin 2108 -> 2108 bytes .../cli/__pycache__/main.cpython-311.pyc | Bin 2363 -> 2363 bytes .../__pycache__/main_parser.cpython-311.pyc | Bin 5522 -> 5522 bytes .../cli/__pycache__/parser.cpython-311.pyc | Bin 17023 -> 17023 bytes .../__pycache__/progress_bars.cpython-311.pyc | Bin 3170 -> 3170 bytes .../__pycache__/req_command.cpython-311.pyc | Bin 20135 -> 20135 bytes .../cli/__pycache__/spinners.cpython-311.pyc | Bin 8835 -> 8835 bytes .../__pycache__/status_codes.cpython-311.pyc | Bin 374 -> 374 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 4454 -> 4454 bytes .../__pycache__/install.cpython-311.pyc | Bin 35373 -> 35373 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 1036 -> 1036 bytes .../__pycache__/base.cpython-311.pyc | Bin 2408 -> 2408 bytes .../__pycache__/installed.cpython-311.pyc | Bin 1545 -> 1545 bytes .../__pycache__/sdist.cpython-311.pyc | Bin 8947 -> 8947 bytes .../__pycache__/wheel.cpython-311.pyc | Bin 2139 -> 2139 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 240 -> 240 bytes .../__pycache__/collector.cpython-311.pyc | Bin 24546 -> 24546 bytes .../package_finder.cpython-311.pyc | Bin 44218 -> 44218 bytes .../index/__pycache__/sources.cpython-311.pyc | Bin 11022 -> 11022 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 18178 -> 18178 bytes .../__pycache__/_sysconfig.cpython-311.pyc | Bin 8882 -> 8882 bytes .../__pycache__/base.cpython-311.pyc | Bin 4003 -> 4003 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 6411 -> 6411 bytes .../__pycache__/_json.cpython-311.pyc | Bin 3565 -> 3565 bytes .../metadata/__pycache__/base.cpython-311.pyc | Bin 38010 -> 38010 bytes .../__pycache__/pkg_resources.cpython-311.pyc | Bin 16858 -> 16858 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 357 -> 357 bytes .../__pycache__/_compat.cpython-311.pyc | Bin 3564 -> 3564 bytes .../__pycache__/_dists.cpython-311.pyc | Bin 14580 -> 14580 bytes .../__pycache__/_envs.cpython-311.pyc | Bin 12418 -> 12418 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 274 -> 274 bytes .../__pycache__/candidate.cpython-311.pyc | Bin 2093 -> 2093 bytes .../__pycache__/direct_url.cpython-311.pyc | Bin 12256 -> 12256 bytes .../format_control.cpython-311.pyc | Bin 4657 -> 4657 bytes .../models/__pycache__/index.cpython-311.pyc | Bin 1899 -> 1899 bytes .../installation_report.cpython-311.pyc | Bin 2613 -> 2613 bytes .../models/__pycache__/link.cpython-311.pyc | Bin 26445 -> 26445 bytes .../models/__pycache__/scheme.cpython-311.pyc | Bin 1265 -> 1265 bytes .../__pycache__/search_scope.cpython-311.pyc | Bin 5828 -> 5828 bytes .../selection_prefs.cpython-311.pyc | Bin 1996 -> 1996 bytes .../__pycache__/target_python.cpython-311.pyc | Bin 4758 -> 4758 bytes .../models/__pycache__/wheel.cpython-311.pyc | Bin 6421 -> 6421 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 262 -> 262 bytes .../network/__pycache__/auth.cpython-311.pyc | Bin 19066 -> 19066 bytes .../network/__pycache__/cache.cpython-311.pyc | Bin 5185 -> 5185 bytes .../__pycache__/download.cpython-311.pyc | Bin 9577 -> 9577 bytes .../__pycache__/lazy_wheel.cpython-311.pyc | Bin 13023 -> 13023 bytes .../__pycache__/session.cpython-311.pyc | Bin 21290 -> 21290 bytes .../network/__pycache__/utils.cpython-311.pyc | Bin 2411 -> 2411 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 200 -> 200 bytes .../__pycache__/check.cpython-311.pyc | Bin 6633 -> 6633 bytes .../__pycache__/prepare.cpython-311.pyc | Bin 26380 -> 26380 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 206 -> 206 bytes .../__pycache__/build_tracker.cpython-311.pyc | Bin 8129 -> 8129 bytes .../__pycache__/metadata.cpython-311.pyc | Bin 2277 -> 2277 bytes .../metadata_editable.cpython-311.pyc | Bin 2313 -> 2313 bytes .../metadata_legacy.cpython-311.pyc | Bin 3713 -> 3713 bytes .../build/__pycache__/wheel.cpython-311.pyc | Bin 1943 -> 1943 bytes .../wheel_editable.cpython-311.pyc | Bin 2387 -> 2387 bytes .../__pycache__/wheel_legacy.cpython-311.pyc | Bin 4494 -> 4494 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 274 -> 274 bytes .../editable_legacy.cpython-311.pyc | Bin 2270 -> 2270 bytes .../__pycache__/legacy.cpython-311.pyc | Bin 6110 -> 6110 bytes .../install/__pycache__/wheel.cpython-311.pyc | Bin 39996 -> 39996 bytes .../req/__pycache__/__init__.cpython-311.pyc | Bin 4446 -> 4446 bytes .../__pycache__/constructors.cpython-311.pyc | Bin 20705 -> 20705 bytes .../req/__pycache__/req_file.cpython-311.pyc | Bin 22434 -> 22434 bytes .../__pycache__/req_install.cpython-311.pyc | Bin 40346 -> 40346 bytes .../req/__pycache__/req_set.cpython-311.pyc | Bin 6002 -> 6002 bytes .../__pycache__/req_uninstall.cpython-311.pyc | Bin 37000 -> 37000 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 200 -> 200 bytes .../__pycache__/base.cpython-311.pyc | Bin 1371 -> 1371 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 211 -> 211 bytes .../__pycache__/base.cpython-311.pyc | Bin 9624 -> 9624 bytes .../__pycache__/candidates.cpython-311.pyc | Bin 28834 -> 28834 bytes .../__pycache__/factory.cpython-311.pyc | Bin 31978 -> 31978 bytes .../found_candidates.cpython-311.pyc | Bin 6759 -> 6759 bytes .../__pycache__/provider.cpython-311.pyc | Bin 11053 -> 11053 bytes .../__pycache__/reporter.cpython-311.pyc | Bin 4656 -> 4656 bytes .../__pycache__/requirements.cpython-311.pyc | Bin 11121 -> 11121 bytes .../__pycache__/resolver.cpython-311.pyc | Bin 12308 -> 12308 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 195 -> 195 bytes .../utils/__pycache__/_log.cpython-311.pyc | Bin 2016 -> 2016 bytes .../utils/__pycache__/appdirs.cpython-311.pyc | Bin 2554 -> 2554 bytes .../utils/__pycache__/compat.cpython-311.pyc | Bin 2262 -> 2262 bytes .../compatibility_tags.cpython-311.pyc | Bin 6754 -> 6754 bytes .../__pycache__/deprecation.cpython-311.pyc | Bin 7085 -> 7085 bytes .../direct_url_helpers.cpython-311.pyc | Bin 3718 -> 3718 bytes .../distutils_args.cpython-311.pyc | Bin 1462 -> 1462 bytes .../__pycache__/egg_link.cpython-311.pyc | Bin 3233 -> 3233 bytes .../__pycache__/encoding.cpython-311.pyc | Bin 2318 -> 2318 bytes .../__pycache__/entrypoints.cpython-311.pyc | Bin 4240 -> 4240 bytes .../__pycache__/filesystem.cpython-311.pyc | Bin 8225 -> 8225 bytes .../__pycache__/filetypes.cpython-311.pyc | Bin 1311 -> 1311 bytes .../utils/__pycache__/glibc.cpython-311.pyc | Bin 2554 -> 2554 bytes .../utils/__pycache__/hashes.cpython-311.pyc | Bin 8332 -> 8332 bytes .../inject_securetransport.cpython-311.pyc | Bin 1329 -> 1329 bytes .../utils/__pycache__/logging.cpython-311.pyc | Bin 15454 -> 15454 bytes .../utils/__pycache__/misc.cpython-311.pyc | Bin 37696 -> 37696 bytes .../utils/__pycache__/models.cpython-311.pyc | Bin 2935 -> 2935 bytes .../__pycache__/packaging.cpython-311.pyc | Bin 2802 -> 2802 bytes .../setuptools_build.cpython-311.pyc | Bin 6099 -> 6099 bytes .../__pycache__/subprocess.cpython-311.pyc | Bin 9889 -> 9889 bytes .../__pycache__/temp_dir.cpython-311.pyc | Bin 11416 -> 11416 bytes .../__pycache__/unpacking.cpython-311.pyc | Bin 12891 -> 12891 bytes .../utils/__pycache__/urls.cpython-311.pyc | Bin 2688 -> 2688 bytes .../__pycache__/virtualenv.cpython-311.pyc | Bin 4935 -> 4935 bytes .../utils/__pycache__/wheel.cpython-311.pyc | Bin 7105 -> 7105 bytes .../vcs/__pycache__/__init__.cpython-311.pyc | Bin 630 -> 630 bytes .../vcs/__pycache__/bazaar.cpython-311.pyc | Bin 5855 -> 5855 bytes .../vcs/__pycache__/git.cpython-311.pyc | Bin 21519 -> 21519 bytes .../vcs/__pycache__/mercurial.cpython-311.pyc | Bin 8701 -> 8701 bytes .../__pycache__/subversion.cpython-311.pyc | Bin 14598 -> 14598 bytes .../versioncontrol.cpython-311.pyc | Bin 31867 -> 31867 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 5608 -> 5608 bytes .../_vendor/__pycache__/six.cpython-311.pyc | Bin 46410 -> 46410 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 837 -> 837 bytes .../__pycache__/adapter.cpython-311.pyc | Bin 5499 -> 5499 bytes .../__pycache__/cache.cpython-311.pyc | Bin 3773 -> 3773 bytes .../__pycache__/compat.cpython-311.pyc | Bin 1130 -> 1130 bytes .../__pycache__/controller.cpython-311.pyc | Bin 16445 -> 16445 bytes .../__pycache__/filewrapper.cpython-311.pyc | Bin 4232 -> 4232 bytes .../__pycache__/serialize.cpython-311.pyc | Bin 8392 -> 8392 bytes .../__pycache__/wrapper.cpython-311.pyc | Bin 958 -> 958 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 412 -> 412 bytes .../__pycache__/file_cache.cpython-311.pyc | Bin 8395 -> 8395 bytes .../__pycache__/redis_cache.cpython-311.pyc | Bin 2492 -> 2492 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 337 -> 337 bytes .../certifi/__pycache__/core.cpython-311.pyc | Bin 3980 -> 3980 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 5069 -> 5069 bytes .../__pycache__/big5freq.cpython-311.pyc | Bin 27199 -> 27199 bytes .../__pycache__/big5prober.cpython-311.pyc | Bin 1674 -> 1674 bytes .../chardistribution.cpython-311.pyc | Bin 11266 -> 11266 bytes .../charsetgroupprober.cpython-311.pyc | Bin 4296 -> 4296 bytes .../__pycache__/charsetprober.cpython-311.pyc | Bin 5542 -> 5542 bytes .../codingstatemachine.cpython-311.pyc | Bin 3993 -> 3993 bytes .../codingstatemachinedict.cpython-311.pyc | Bin 949 -> 949 bytes .../__pycache__/cp949prober.cpython-311.pyc | Bin 1683 -> 1683 bytes .../chardet/__pycache__/enums.cpython-311.pyc | Bin 3384 -> 3384 bytes .../__pycache__/escprober.cpython-311.pyc | Bin 4900 -> 4900 bytes .../chardet/__pycache__/escsm.cpython-311.pyc | Bin 12639 -> 12639 bytes .../__pycache__/eucjpprober.cpython-311.pyc | Bin 4726 -> 4726 bytes .../__pycache__/euckrfreq.cpython-311.pyc | Bin 12082 -> 12082 bytes .../__pycache__/euckrprober.cpython-311.pyc | Bin 1675 -> 1675 bytes .../__pycache__/euctwfreq.cpython-311.pyc | Bin 27204 -> 27204 bytes .../__pycache__/euctwprober.cpython-311.pyc | Bin 1675 -> 1675 bytes .../__pycache__/gb2312freq.cpython-311.pyc | Bin 19126 -> 19126 bytes .../__pycache__/gb2312prober.cpython-311.pyc | Bin 1690 -> 1690 bytes .../__pycache__/hebrewprober.cpython-311.pyc | Bin 5679 -> 5679 bytes .../__pycache__/jisfreq.cpython-311.pyc | Bin 22155 -> 22155 bytes .../__pycache__/johabfreq.cpython-311.pyc | Bin 84659 -> 84659 bytes .../__pycache__/johabprober.cpython-311.pyc | Bin 1681 -> 1681 bytes .../__pycache__/jpcntx.cpython-311.pyc | Bin 40163 -> 40163 bytes .../langbulgarianmodel.cpython-311.pyc | Bin 85833 -> 85833 bytes .../langgreekmodel.cpython-311.pyc | Bin 79255 -> 79255 bytes .../langhebrewmodel.cpython-311.pyc | Bin 80017 -> 80017 bytes .../langrussianmodel.cpython-311.pyc | Bin 108734 -> 108734 bytes .../__pycache__/langthaimodel.cpython-311.pyc | Bin 80195 -> 80195 bytes .../langturkishmodel.cpython-311.pyc | Bin 80034 -> 80034 bytes .../__pycache__/latin1prober.cpython-311.pyc | Bin 7330 -> 7330 bytes .../macromanprober.cpython-311.pyc | Bin 7497 -> 7497 bytes .../mbcharsetprober.cpython-311.pyc | Bin 4118 -> 4118 bytes .../mbcsgroupprober.cpython-311.pyc | Bin 1988 -> 1988 bytes .../__pycache__/mbcssm.cpython-311.pyc | Bin 31728 -> 31728 bytes .../__pycache__/resultdict.cpython-311.pyc | Bin 767 -> 767 bytes .../sbcharsetprober.cpython-311.pyc | Bin 6393 -> 6393 bytes .../sbcsgroupprober.cpython-311.pyc | Bin 2938 -> 2938 bytes .../__pycache__/sjisprober.cpython-311.pyc | Bin 4831 -> 4831 bytes .../universaldetector.cpython-311.pyc | Bin 12459 -> 12459 bytes .../__pycache__/utf1632prober.cpython-311.pyc | Bin 10579 -> 10579 bytes .../__pycache__/utf8prober.cpython-311.pyc | Bin 3466 -> 3466 bytes .../__pycache__/version.cpython-311.pyc | Bin 502 -> 502 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 1440 -> 1440 bytes .../__pycache__/compat.cpython-311.pyc | Bin 52306 -> 52306 bytes .../__pycache__/resources.cpython-311.pyc | Bin 18989 -> 18989 bytes .../__pycache__/scripts.cpython-311.pyc | Bin 21265 -> 21265 bytes .../distlib/__pycache__/util.cpython-311.pyc | Bin 97444 -> 97444 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 1193 -> 1193 bytes .../distro/__pycache__/distro.cpython-311.pyc | Bin 57726 -> 57726 bytes .../idna/__pycache__/__init__.cpython-311.pyc | Bin 1094 -> 1094 bytes .../idna/__pycache__/core.cpython-311.pyc | Bin 19446 -> 19446 bytes .../idna/__pycache__/idnadata.cpython-311.pyc | Bin 38970 -> 38970 bytes .../__pycache__/intranges.cpython-311.pyc | Bin 2979 -> 2979 bytes .../__pycache__/package_data.cpython-311.pyc | Bin 214 -> 214 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 2073 -> 2073 bytes .../__pycache__/exceptions.cpython-311.pyc | Bin 2374 -> 2374 bytes .../msgpack/__pycache__/ext.cpython-311.pyc | Bin 9160 -> 9160 bytes .../__pycache__/fallback.cpython-311.pyc | Bin 47187 -> 47187 bytes .../__pycache__/__about__.cpython-311.pyc | Bin 638 -> 638 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 559 -> 559 bytes .../__pycache__/_manylinux.cpython-311.pyc | Bin 13225 -> 13225 bytes .../__pycache__/_musllinux.cpython-311.pyc | Bin 7993 -> 7993 bytes .../__pycache__/_structures.cpython-311.pyc | Bin 3681 -> 3681 bytes .../__pycache__/markers.cpython-311.pyc | Bin 16521 -> 16521 bytes .../__pycache__/requirements.cpython-311.pyc | Bin 7636 -> 7636 bytes .../__pycache__/specifiers.cpython-311.pyc | Bin 34359 -> 34359 bytes .../__pycache__/tags.cpython-311.pyc | Bin 21344 -> 21344 bytes .../__pycache__/utils.cpython-311.pyc | Bin 6679 -> 6679 bytes .../__pycache__/version.cpython-311.pyc | Bin 21871 -> 21871 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 158227 -> 158227 bytes .../__pycache__/py31compat.cpython-311.pyc | Bin 980 -> 980 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 12926 -> 12926 bytes .../__pycache__/api.cpython-311.pyc | Bin 7180 -> 7180 bytes .../__pycache__/unix.cpython-311.pyc | Bin 11022 -> 11022 bytes .../__pycache__/version.cpython-311.pyc | Bin 309 -> 309 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 3841 -> 3841 bytes .../__pycache__/filter.cpython-311.pyc | Bin 3501 -> 3501 bytes .../__pycache__/lexer.cpython-311.pyc | Bin 40395 -> 40395 bytes .../__pycache__/modeline.cpython-311.pyc | Bin 1720 -> 1720 bytes .../__pycache__/plugin.cpython-311.pyc | Bin 3733 -> 3733 bytes .../__pycache__/regexopt.cpython-311.pyc | Bin 5027 -> 5027 bytes .../__pycache__/style.cpython-311.pyc | Bin 7421 -> 7421 bytes .../__pycache__/token.cpython-311.pyc | Bin 7461 -> 7461 bytes .../pygments/__pycache__/util.cpython-311.pyc | Bin 14588 -> 14588 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 40101 -> 40101 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 15144 -> 15144 bytes .../__pycache__/_mapping.cpython-311.pyc | Bin 62775 -> 62775 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 4454 -> 4454 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 8344 -> 8344 bytes .../__pycache__/actions.cpython-311.pyc | Bin 8458 -> 8458 bytes .../__pycache__/common.cpython-311.pyc | Bin 14780 -> 14780 bytes .../__pycache__/core.cpython-311.pyc | Bin 277666 -> 277666 bytes .../__pycache__/exceptions.cpython-311.pyc | Bin 12922 -> 12922 bytes .../__pycache__/helpers.cpython-311.pyc | Bin 53623 -> 53623 bytes .../__pycache__/results.cpython-311.pyc | Bin 36306 -> 36306 bytes .../__pycache__/testing.cpython-311.pyc | Bin 19502 -> 19502 bytes .../__pycache__/unicode.cpython-311.pyc | Bin 15360 -> 15360 bytes .../__pycache__/util.cpython-311.pyc | Bin 14259 -> 14259 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 702 -> 702 bytes .../__pycache__/_impl.cpython-311.pyc | Bin 16666 -> 16666 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 1162 -> 1162 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 6446 -> 6446 bytes .../__pycache__/__version__.cpython-311.pyc | Bin 583 -> 583 bytes .../_internal_utils.cpython-311.pyc | Bin 2080 -> 2080 bytes .../__pycache__/adapters.cpython-311.pyc | Bin 24883 -> 24883 bytes .../requests/__pycache__/api.cpython-311.pyc | Bin 7428 -> 7428 bytes .../requests/__pycache__/auth.cpython-311.pyc | Bin 14627 -> 14627 bytes .../__pycache__/certs.cpython-311.pyc | Bin 979 -> 979 bytes .../__pycache__/compat.cpython-311.pyc | Bin 1805 -> 1805 bytes .../__pycache__/cookies.cpython-311.pyc | Bin 27107 -> 27107 bytes .../__pycache__/exceptions.cpython-311.pyc | Bin 8522 -> 8522 bytes .../__pycache__/hooks.cpython-311.pyc | Bin 1247 -> 1247 bytes .../__pycache__/models.cpython-311.pyc | Bin 38778 -> 38778 bytes .../__pycache__/packages.cpython-311.pyc | Bin 827 -> 827 bytes .../__pycache__/sessions.cpython-311.pyc | Bin 29616 -> 29616 bytes .../__pycache__/status_codes.cpython-311.pyc | Bin 6234 -> 6234 bytes .../__pycache__/structures.cpython-311.pyc | Bin 6219 -> 6219 bytes .../__pycache__/utils.cpython-311.pyc | Bin 40133 -> 40133 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 750 -> 750 bytes .../__pycache__/providers.cpython-311.pyc | Bin 7069 -> 7069 bytes .../__pycache__/reporters.cpython-311.pyc | Bin 2799 -> 2799 bytes .../__pycache__/resolvers.cpython-311.pyc | Bin 25245 -> 25245 bytes .../__pycache__/structs.cpython-311.pyc | Bin 11327 -> 11327 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 205 -> 205 bytes .../collections_abc.cpython-311.pyc | Bin 480 -> 480 bytes .../rich/__pycache__/__init__.cpython-311.pyc | Bin 7493 -> 7493 bytes .../__pycache__/_cell_widths.cpython-311.pyc | Bin 7832 -> 7832 bytes .../__pycache__/_emoji_codes.cpython-311.pyc | Bin 208519 -> 208519 bytes .../_emoji_replace.cpython-311.pyc | Bin 1931 -> 1931 bytes .../_export_format.cpython-311.pyc | Bin 2336 -> 2336 bytes .../__pycache__/_extension.cpython-311.pyc | Bin 632 -> 632 bytes .../__pycache__/_log_render.cpython-311.pyc | Bin 4766 -> 4766 bytes .../rich/__pycache__/_loop.cpython-311.pyc | Bin 2112 -> 2112 bytes .../__pycache__/_null_file.cpython-311.pyc | Bin 4677 -> 4677 bytes .../__pycache__/_palettes.cpython-311.pyc | Bin 5248 -> 5248 bytes .../rich/__pycache__/_pick.cpython-311.pyc | Bin 793 -> 793 bytes .../rich/__pycache__/_ratio.cpython-311.pyc | Bin 7931 -> 7931 bytes .../__pycache__/_spinners.cpython-311.pyc | Bin 13681 -> 13681 bytes .../rich/__pycache__/_wrap.cpython-311.pyc | Bin 2783 -> 2783 bytes .../rich/__pycache__/abc.cpython-311.pyc | Bin 1924 -> 1924 bytes .../rich/__pycache__/align.cpython-311.pyc | Bin 13473 -> 13473 bytes .../rich/__pycache__/ansi.cpython-311.pyc | Bin 10449 -> 10449 bytes .../rich/__pycache__/box.cpython-311.pyc | Bin 12988 -> 12988 bytes .../rich/__pycache__/cells.cpython-311.pyc | Bin 6438 -> 6438 bytes .../rich/__pycache__/color.cpython-311.pyc | Bin 27569 -> 27569 bytes .../__pycache__/color_triplet.cpython-311.pyc | Bin 1872 -> 1872 bytes .../rich/__pycache__/columns.cpython-311.pyc | Bin 10643 -> 10643 bytes .../rich/__pycache__/console.cpython-311.pyc | Bin 123159 -> 123159 bytes .../__pycache__/constrain.cpython-311.pyc | Bin 2464 -> 2464 bytes .../__pycache__/containers.cpython-311.pyc | Bin 10805 -> 10805 bytes .../rich/__pycache__/control.cpython-311.pyc | Bin 11896 -> 11896 bytes .../default_styles.cpython-311.pyc | Bin 12496 -> 12496 bytes .../rich/__pycache__/emoji.cpython-311.pyc | Bin 4797 -> 4797 bytes .../rich/__pycache__/errors.cpython-311.pyc | Bin 2328 -> 2328 bytes .../__pycache__/file_proxy.cpython-311.pyc | Bin 3776 -> 3776 bytes .../rich/__pycache__/filesize.cpython-311.pyc | Bin 3300 -> 3300 bytes .../__pycache__/highlighter.cpython-311.pyc | Bin 10987 -> 10987 bytes .../rich/__pycache__/jupyter.cpython-311.pyc | Bin 6403 -> 6403 bytes .../rich/__pycache__/live.cpython-311.pyc | Bin 21131 -> 21131 bytes .../__pycache__/live_render.cpython-311.pyc | Bin 5144 -> 5144 bytes .../rich/__pycache__/logging.cpython-311.pyc | Bin 14515 -> 14515 bytes .../rich/__pycache__/markup.cpython-311.pyc | Bin 10437 -> 10437 bytes .../rich/__pycache__/measure.cpython-311.pyc | Bin 7270 -> 7270 bytes .../rich/__pycache__/padding.cpython-311.pyc | Bin 7486 -> 7486 bytes .../rich/__pycache__/pager.cpython-311.pyc | Bin 2244 -> 2244 bytes .../rich/__pycache__/palette.cpython-311.pyc | Bin 5977 -> 5977 bytes .../rich/__pycache__/panel.cpython-311.pyc | Bin 12733 -> 12733 bytes .../rich/__pycache__/pretty.cpython-311.pyc | Bin 44826 -> 44826 bytes .../rich/__pycache__/progress.cpython-311.pyc | Bin 82705 -> 82705 bytes .../__pycache__/progress_bar.cpython-311.pyc | Bin 11011 -> 11011 bytes .../rich/__pycache__/protocol.cpython-311.pyc | Bin 2095 -> 2095 bytes .../rich/__pycache__/region.cpython-311.pyc | Bin 658 -> 658 bytes .../rich/__pycache__/repr.cpython-311.pyc | Bin 7657 -> 7657 bytes .../rich/__pycache__/scope.cpython-311.pyc | Bin 4350 -> 4350 bytes .../rich/__pycache__/screen.cpython-311.pyc | Bin 2773 -> 2773 bytes .../rich/__pycache__/segment.cpython-311.pyc | Bin 31540 -> 31540 bytes .../rich/__pycache__/spinner.cpython-311.pyc | Bin 6889 -> 6889 bytes .../rich/__pycache__/style.cpython-311.pyc | Bin 34330 -> 34330 bytes .../rich/__pycache__/styled.cpython-311.pyc | Bin 2438 -> 2438 bytes .../rich/__pycache__/syntax.cpython-311.pyc | Bin 42532 -> 42532 bytes .../rich/__pycache__/table.cpython-311.pyc | Bin 48799 -> 48799 bytes .../terminal_theme.cpython-311.pyc | Bin 3704 -> 3704 bytes .../rich/__pycache__/text.cpython-311.pyc | Bin 65215 -> 65215 bytes .../rich/__pycache__/theme.cpython-311.pyc | Bin 7142 -> 7142 bytes .../rich/__pycache__/themes.cpython-311.pyc | Bin 354 -> 354 bytes .../__pycache__/traceback.cpython-311.pyc | Bin 31668 -> 31668 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 27792 -> 27792 bytes .../__pycache__/_asyncio.cpython-311.pyc | Bin 4799 -> 4799 bytes .../__pycache__/_utils.cpython-311.pyc | Bin 2064 -> 2064 bytes .../__pycache__/after.cpython-311.pyc | Bin 1691 -> 1691 bytes .../__pycache__/before.cpython-311.pyc | Bin 1525 -> 1525 bytes .../__pycache__/before_sleep.cpython-311.pyc | Bin 2102 -> 2102 bytes .../tenacity/__pycache__/nap.cpython-311.pyc | Bin 1564 -> 1564 bytes .../__pycache__/retry.cpython-311.pyc | Bin 15038 -> 15038 bytes .../tenacity/__pycache__/stop.cpython-311.pyc | Bin 5892 -> 5892 bytes .../tenacity/__pycache__/wait.cpython-311.pyc | Bin 13364 -> 13364 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 409 -> 409 bytes .../tomli/__pycache__/_parser.cpython-311.pyc | Bin 30848 -> 30848 bytes .../tomli/__pycache__/_re.cpython-311.pyc | Bin 4488 -> 4488 bytes .../tomli/__pycache__/_types.cpython-311.pyc | Bin 401 -> 401 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 3707 -> 3707 bytes .../__pycache__/_collections.cpython-311.pyc | Bin 18295 -> 18295 bytes .../__pycache__/_version.cpython-311.pyc | Bin 217 -> 217 bytes .../__pycache__/connection.cpython-311.pyc | Bin 21891 -> 21891 bytes .../connectionpool.cpython-311.pyc | Bin 37634 -> 37634 bytes .../__pycache__/exceptions.cpython-311.pyc | Bin 16121 -> 16121 bytes .../__pycache__/fields.cpython-311.pyc | Bin 11414 -> 11414 bytes .../__pycache__/filepost.cpython-311.pyc | Bin 4495 -> 4495 bytes .../__pycache__/poolmanager.cpython-311.pyc | Bin 21818 -> 21818 bytes .../__pycache__/request.cpython-311.pyc | Bin 6658 -> 6658 bytes .../__pycache__/response.cpython-311.pyc | Bin 36541 -> 36541 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 203 -> 203 bytes .../_appengine_environ.cpython-311.pyc | Bin 1942 -> 1942 bytes .../contrib/__pycache__/socks.cpython-311.pyc | Bin 8087 -> 8087 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 204 -> 204 bytes .../packages/__pycache__/six.cpython-311.pyc | Bin 46446 -> 46446 bytes .../util/__pycache__/__init__.cpython-311.pyc | Bin 1406 -> 1406 bytes .../__pycache__/connection.cpython-311.pyc | Bin 5133 -> 5133 bytes .../util/__pycache__/proxy.cpython-311.pyc | Bin 1715 -> 1715 bytes .../util/__pycache__/queue.cpython-311.pyc | Bin 1498 -> 1498 bytes .../util/__pycache__/request.cpython-311.pyc | Bin 4618 -> 4618 bytes .../util/__pycache__/response.cpython-311.pyc | Bin 3487 -> 3487 bytes .../util/__pycache__/retry.cpython-311.pyc | Bin 22755 -> 22755 bytes .../util/__pycache__/ssl_.cpython-311.pyc | Bin 16818 -> 16818 bytes .../ssl_match_hostname.cpython-311.pyc | Bin 5797 -> 5797 bytes .../__pycache__/ssltransport.cpython-311.pyc | Bin 11626 -> 11626 bytes .../util/__pycache__/timeout.cpython-311.pyc | Bin 11034 -> 11034 bytes .../util/__pycache__/url.cpython-311.pyc | Bin 17558 -> 17558 bytes .../util/__pycache__/wait.cpython-311.pyc | Bin 5000 -> 5000 bytes .../yaml/__pycache__/__init__.cpython-311.pyc | Bin 17227 -> 17227 bytes .../yaml/__pycache__/composer.cpython-311.pyc | Bin 7066 -> 7066 bytes .../__pycache__/constructor.cpython-311.pyc | Bin 38657 -> 38657 bytes .../yaml/__pycache__/cyaml.cpython-311.pyc | Bin 5401 -> 5401 bytes .../yaml/__pycache__/dumper.cpython-311.pyc | Bin 2868 -> 2868 bytes .../yaml/__pycache__/emitter.cpython-311.pyc | Bin 53394 -> 53394 bytes .../yaml/__pycache__/error.cpython-311.pyc | Bin 4260 -> 4260 bytes .../yaml/__pycache__/events.cpython-311.pyc | Bin 5859 -> 5859 bytes .../yaml/__pycache__/loader.cpython-311.pyc | Bin 4255 -> 4255 bytes .../yaml/__pycache__/nodes.cpython-311.pyc | Bin 2531 -> 2531 bytes .../yaml/__pycache__/parser.cpython-311.pyc | Bin 25764 -> 25764 bytes .../yaml/__pycache__/reader.cpython-311.pyc | Bin 8934 -> 8934 bytes .../__pycache__/representer.cpython-311.pyc | Bin 18364 -> 18364 bytes .../yaml/__pycache__/resolver.cpython-311.pyc | Bin 9904 -> 9904 bytes .../yaml/__pycache__/scanner.cpython-311.pyc | Bin 57118 -> 57118 bytes .../__pycache__/serializer.cpython-311.pyc | Bin 6717 -> 6717 bytes .../yaml/__pycache__/tokens.cpython-311.pyc | Bin 7254 -> 7254 bytes 941 files changed, 61057 insertions(+) create mode 100644 ansible/lib/python3.11/site-packages/passlib-1.7.4.dist-info/INSTALLER create mode 100644 ansible/lib/python3.11/site-packages/passlib-1.7.4.dist-info/LICENSE create mode 100644 ansible/lib/python3.11/site-packages/passlib-1.7.4.dist-info/METADATA create mode 100644 ansible/lib/python3.11/site-packages/passlib-1.7.4.dist-info/RECORD create mode 100644 ansible/lib/python3.11/site-packages/passlib-1.7.4.dist-info/REQUESTED create mode 100644 ansible/lib/python3.11/site-packages/passlib-1.7.4.dist-info/WHEEL create mode 100644 ansible/lib/python3.11/site-packages/passlib-1.7.4.dist-info/top_level.txt create mode 100644 ansible/lib/python3.11/site-packages/passlib-1.7.4.dist-info/zip-safe create mode 100644 ansible/lib/python3.11/site-packages/passlib/__init__.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/__pycache__/__init__.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/__pycache__/apache.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/__pycache__/apps.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/__pycache__/context.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/__pycache__/exc.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/__pycache__/hash.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/__pycache__/hosts.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/__pycache__/ifc.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/__pycache__/pwd.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/__pycache__/registry.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/__pycache__/totp.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/__pycache__/win32.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/_data/wordsets/bip39.txt create mode 100644 ansible/lib/python3.11/site-packages/passlib/_data/wordsets/eff_long.txt create mode 100644 ansible/lib/python3.11/site-packages/passlib/_data/wordsets/eff_prefixed.txt create mode 100644 ansible/lib/python3.11/site-packages/passlib/_data/wordsets/eff_short.txt create mode 100644 ansible/lib/python3.11/site-packages/passlib/apache.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/apps.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/context.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/crypto/__init__.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/crypto/__pycache__/__init__.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/crypto/__pycache__/_md4.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/crypto/__pycache__/des.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/crypto/__pycache__/digest.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/crypto/_blowfish/__init__.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/crypto/_blowfish/__pycache__/__init__.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/crypto/_blowfish/__pycache__/_gen_files.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/crypto/_blowfish/__pycache__/base.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/crypto/_blowfish/__pycache__/unrolled.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/crypto/_blowfish/_gen_files.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/crypto/_blowfish/base.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/crypto/_blowfish/unrolled.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/crypto/_md4.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/crypto/des.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/crypto/digest.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/crypto/scrypt/__init__.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/crypto/scrypt/__pycache__/__init__.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/crypto/scrypt/__pycache__/_builtin.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/crypto/scrypt/__pycache__/_gen_files.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/crypto/scrypt/__pycache__/_salsa.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/crypto/scrypt/_builtin.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/crypto/scrypt/_gen_files.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/crypto/scrypt/_salsa.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/exc.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/ext/__init__.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/ext/__pycache__/__init__.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/ext/django/__init__.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/ext/django/__pycache__/__init__.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/ext/django/__pycache__/models.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/ext/django/__pycache__/utils.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/ext/django/models.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/ext/django/utils.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/__init__.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/__init__.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/argon2.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/bcrypt.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/cisco.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/des_crypt.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/digests.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/django.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/fshp.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/ldap_digests.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/md5_crypt.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/misc.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/mssql.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/mysql.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/oracle.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/pbkdf2.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/phpass.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/postgres.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/roundup.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/scram.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/scrypt.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/sha1_crypt.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/sha2_crypt.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/sun_md5_crypt.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/windows.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/argon2.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/bcrypt.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/cisco.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/des_crypt.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/digests.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/django.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/fshp.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/ldap_digests.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/md5_crypt.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/misc.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/mssql.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/mysql.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/oracle.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/pbkdf2.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/phpass.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/postgres.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/roundup.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/scram.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/scrypt.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/sha1_crypt.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/sha2_crypt.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/sun_md5_crypt.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/handlers/windows.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/hash.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/hosts.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/ifc.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/pwd.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/registry.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/__init__.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/__main__.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/__init__.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/__main__.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/_test_bad_register.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/backports.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_apache.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_apps.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_context.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_context_deprecated.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_crypto_builtin_md4.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_crypto_des.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_crypto_digest.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_crypto_scrypt.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_ext_django.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_ext_django_source.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_handlers.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_handlers_argon2.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_handlers_bcrypt.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_handlers_cisco.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_handlers_django.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_handlers_pbkdf2.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_handlers_scrypt.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_hosts.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_pwd.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_registry.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_totp.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_utils.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_utils_handlers.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_utils_md4.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_utils_pbkdf2.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_win32.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/tox_support.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/utils.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/_test_bad_register.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/backports.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/sample1.cfg create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/sample1b.cfg create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/sample1c.cfg create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/sample_config_1s.cfg create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/test_apache.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/test_apps.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/test_context.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/test_context_deprecated.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/test_crypto_builtin_md4.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/test_crypto_des.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/test_crypto_digest.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/test_crypto_scrypt.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/test_ext_django.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/test_ext_django_source.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/test_handlers.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/test_handlers_argon2.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/test_handlers_bcrypt.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/test_handlers_cisco.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/test_handlers_django.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/test_handlers_pbkdf2.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/test_handlers_scrypt.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/test_hosts.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/test_pwd.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/test_registry.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/test_totp.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/test_utils.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/test_utils_handlers.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/test_utils_md4.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/test_utils_pbkdf2.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/test_win32.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/tox_support.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/tests/utils.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/totp.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/utils/__init__.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/utils/__pycache__/__init__.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/utils/__pycache__/binary.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/utils/__pycache__/decor.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/utils/__pycache__/des.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/utils/__pycache__/handlers.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/utils/__pycache__/md4.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/utils/__pycache__/pbkdf2.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/utils/binary.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/utils/compat/__init__.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/utils/compat/__pycache__/__init__.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/utils/compat/__pycache__/_ordered_dict.cpython-311.pyc create mode 100644 ansible/lib/python3.11/site-packages/passlib/utils/compat/_ordered_dict.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/utils/decor.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/utils/des.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/utils/handlers.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/utils/md4.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/utils/pbkdf2.py create mode 100644 ansible/lib/python3.11/site-packages/passlib/win32.py diff --git a/ansible/lib/python3.11/site-packages/_distutils_hack/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/_distutils_hack/__pycache__/__init__.cpython-311.pyc index 2d1b246c722bcc0734f04adc5d51e7a1fdf8f99d..5270407d7c29c426f293027888cb3ad045466531 100644 GIT binary patch delta 20 acmbOdJ|&!cIWI340}yO6+_I6oQyTz0PzAOC delta 20 acmbOdJ|&!cIWI340}#ABw0R?Ur#1jVM+SfZ diff --git a/ansible/lib/python3.11/site-packages/ansible/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/__pycache__/__init__.cpython-311.pyc index 4db3ee81b1533a0158780b3b4de1af76a48fe546..c32d3949b688ed09ff91f076f870de406e63f848 100644 GIT binary patch delta 20 acmZo=Ze`|P&dbZi00dhMw`}CDVgvv!{sgxG delta 20 acmZo=Ze`|P&dbZi00h4dZQjUT#Rvd1Nd=7n diff --git a/ansible/lib/python3.11/site-packages/ansible/__pycache__/constants.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/__pycache__/constants.cpython-311.pyc index 2c9884a06598927e6c1de8746aa00eb9c0d641d8..0406e24a21e9e23b407bae0cb64b8b2c904b846d 100644 GIT binary patch delta 20 acmbOiHZP2OIWI340}yO6+_I5dSQ`L5yaiGK delta 20 acmbOiHZP2OIWI340}%W=w0R@9ur>ff2L>nr diff --git a/ansible/lib/python3.11/site-packages/ansible/__pycache__/context.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/__pycache__/context.cpython-311.pyc index 95c9220d74d90179d8c841021fd291fbf9115981..da78a789ded2afda13f18c73b9291b354a68c6ce 100644 GIT binary patch delta 20 acmbO#G*yUuIWI340}yO6+_I6IixU7bq6Ccq delta 20 acmbO#G*yUuIWI340}%W=w0R>p7bgHV?FC~1 diff --git a/ansible/lib/python3.11/site-packages/ansible/__pycache__/release.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/__pycache__/release.cpython-311.pyc index b1cc2784c8af203a3e78cd6560554c2f5a0b40ea..b9c242c9a3bc95f8b363af6b635d9e390aa02d5f 100644 GIT binary patch delta 20 acmcc1beD;HIWI340}yO6+_I57h!Frfngu-o delta 20 acmcc1beD;HIWI340}%W=w0R?U5F-FTV;$F_n%f$c$TMV~sV;$F_n%f$c$zYcBQ$j#CS07t9eIWI340}yO6+_I7TggyXAOa_Ml delta 20 acmcZ{e>t9eIWI340}%W=w0R@<34H)emj_({ diff --git a/ansible/lib/python3.11/site-packages/ansible/cli/arguments/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/cli/arguments/__pycache__/__init__.cpython-311.pyc index c14a3bece79e1801b7f4b5f1d3ebe3bad46bfa13..5c822301f55992dbd7400675f5add0769fe9613e 100644 GIT binary patch delta 19 ZcmZo>YG&eI&dbZi00dhMw@l>z0RSlb1r7iJ delta 19 ZcmZo>YG&eI&dbZi00h4dZJx;e0{|^>1>^t# diff --git a/ansible/lib/python3.11/site-packages/ansible/cli/arguments/__pycache__/option_helpers.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/cli/arguments/__pycache__/option_helpers.cpython-311.pyc index d699f3c2f8690b3bca03e94d033193f8903e1d37..ff2c392fa8b8f0248c50adcf34fc4e474982c207 100644 GIT binary patch delta 22 ccmeCZ#n^j`k$X8WFBbz4Y%$!jk(((O08v5)YXATM delta 22 ccmeCZ#n^j`k$X8WFBbz4{5rIGBR5kn09K6$K>z>% diff --git a/ansible/lib/python3.11/site-packages/ansible/cli/scripts/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/cli/scripts/__pycache__/__init__.cpython-311.pyc index e93332714059ad76a88770a5453f10b776af148e..fecce2b797162f4e0e1c20e8c03aacc68a4b053f 100644 GIT binary patch delta 19 ZcmX@ic$krUIWI340}yO6+%l1S2LLgd1xx?{ delta 19 ZcmX@ic$krUIWI340}%W=w0R=;4gfX|1|k3e diff --git a/ansible/lib/python3.11/site-packages/ansible/config/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/config/__pycache__/__init__.cpython-311.pyc index 4f9ab5cb53772b06c1f2491d2c67fbb35d1c7817..3c14c133e93f0a23f7ec138fb668d5b34f23928c 100644 GIT binary patch delta 19 ZcmdnTxQ~&0IWI340}yO6+%l1SGXOB)1w8-& delta 19 ZcmdnTxQ~&0IWI340}%W=w0R=;W&kx+1`_}P diff --git a/ansible/lib/python3.11/site-packages/ansible/config/__pycache__/manager.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/config/__pycache__/manager.cpython-311.pyc index 00cdc1a92f54df2e3a2d60c1878c6cf6f05c9577..aaf0e906a51151edff302ed4f8b719ec05f6768e 100644 GIT binary patch delta 22 ccmdng!L+G^iF-LOFBbz4Y%$!jkz2h5082LpPXGV_ delta 22 ccmdng!L+G^iF-LOFBbz4{5rIGBe!}B08oMlB>(^b diff --git a/ansible/lib/python3.11/site-packages/ansible/errors/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/errors/__pycache__/__init__.cpython-311.pyc index 6e548d72337ec9269c2987dbf956f46ee7ada792..1fd40ad0253e72ecf27ce169b65575e66e78c88e 100644 GIT binary patch delta 22 ccmaDciSflGM(*Xjyj%=Gu*Go8M($Wo099`W$N&HU delta 22 ccmaDciSflGM(*Xjyj%=G@axd#joh)G09v{So&W#< diff --git a/ansible/lib/python3.11/site-packages/ansible/errors/__pycache__/yaml_strings.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/errors/__pycache__/yaml_strings.cpython-311.pyc index b10eb82973bfbc91ca4ffe81b6c73160bab5869f..ee51f81ce84ce7f9a222459175565a5eb808910f 100644 GIT binary patch delta 20 acmeB_?3CnQ&dbZi00dhMw`}CD;{gCM!36pM delta 20 acmeB_?3CnQ&dbZi00h4dZQjUT#{&R23Yl$IWI340}yO6+%l2-GXOdu1^oa3 delta 19 Zcmeyz_>Yl$IWI340}%W=w0R=;X8=Bx2Gall diff --git a/ansible/lib/python3.11/site-packages/ansible/executor/__pycache__/action_write_locks.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/executor/__pycache__/action_write_locks.cpython-311.pyc index 8c7559d9a9efbd00fe75e9720e922a7e453fcb73..09870b7cdbdd79cc98e90775fac1b090657e5a89 100644 GIT binary patch delta 20 acmeyx`HPc#IWI340}yO6+_I7TJqrLqAqFJ? delta 20 acmeyx`HPc#IWI340}%W=w0R@z4FD->1s4DS delta 19 ZcmZo;YGdMF&dbZi00h4dZJx;e8vrfe1?>O; diff --git a/ansible/lib/python3.11/site-packages/ansible/executor/process/__pycache__/worker.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/executor/process/__pycache__/worker.cpython-311.pyc index 6d535fc9be7ece2e8bf9c6db4e66433627ee9418..d726890083d52bba2ac7693e717a232f45892fc1 100644 GIT binary patch delta 20 acmcZ-eI=TEIWI340}yO6+_I7Tq%HtP;s$^K delta 20 acmcZ-eI=TEIWI340}%W=w0R@ delta 19 ZcmX@ec#x5MIWI340}%W=w0R=;HUKrm1{?qY diff --git a/ansible/lib/python3.11/site-packages/ansible/inventory/__pycache__/data.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/inventory/__pycache__/data.cpython-311.pyc index f91f4e2a8bfc92b3e4672a4afe9ce06dfcbde1f2..0ce38995bc9566cac908a79aef59fdeb5a2243d6 100644 GIT binary patch delta 20 acmaEs@+^gWIWI340}yO6+_I57+6Vwi`vy$_ delta 20 acmaEs@+^gWIWI340}%W=w0R?Uv=IPLMh7DR diff --git a/ansible/lib/python3.11/site-packages/ansible/inventory/__pycache__/group.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/inventory/__pycache__/group.cpython-311.pyc index 375cc17363929a8482d6d5c4533a69859436b178..b6f1e176d2e8578aa24eb518e9e13795cace5e02 100644 GIT binary patch delta 20 acmX?{e>k6eIWI340}yO6+_I5-hdBU86b63) delta 20 acmX?{e>k6eIWI340}%W=w0R@<4s!rZUk6nH diff --git a/ansible/lib/python3.11/site-packages/ansible/inventory/__pycache__/helpers.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/inventory/__pycache__/helpers.cpython-311.pyc index 35d7f97a47f4658018a6f7c309eca6530e6e95ce..ecba5d8c4f5ba99bd868daeb61da3c8de1176547 100644 GIT binary patch delta 20 acmZ3;xsa24IWI340}yO6+_I5-8Vdk3IRzR3 delta 20 acmZ3;xsa24IWI340}%W=w0R@>S delta 20 acmZ2yx6Y1xIWI340}%W=w0R@9vOEAm8wMNz diff --git a/ansible/lib/python3.11/site-packages/ansible/inventory/__pycache__/manager.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/inventory/__pycache__/manager.cpython-311.pyc index e1a162be1e6dd2cea18c8189943818c1bc9977d6..6ed3a33c2cbc528f3a06b197e4514e5dfeae7f40 100644 GIT binary patch delta 29 kcmaDem+8%1Chq0Dyj%=Gu*Go8M(#)HjJGzwOaG?^0G*2qk^lez delta 29 kcmaDem+8%1Chq0Dyj%=G@axd#jogpY8Gmhlm;O%;0I6^chyVZp diff --git a/ansible/lib/python3.11/site-packages/ansible/module_utils/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/module_utils/__pycache__/__init__.cpython-311.pyc index db801c25bd8998ebeb4a7ff840ef0c29d12cac7d..be6159864368dfb6bcc972c2c65bd7bf3743bc89 100644 GIT binary patch delta 19 ZcmX@Yc!ZIAIWI340}yO6+%l1SCjc?N1y29~ delta 19 ZcmX@Yc!ZIAIWI340}%W=w0R=;P5?GD1|d@Y!JIWI340}yO6+_I7Tv?c&X4hC`n delta 20 acmcZ>d@Y!JIWI340}%W=w0R@&Ia}X diff --git a/ansible/lib/python3.11/site-packages/ansible/module_utils/__pycache__/json_utils.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/module_utils/__pycache__/json_utils.cpython-311.pyc index 29f6d53c8a20ba32656c19d083378a0d49a3eab7..3b25c1a8b2dce3ecea3baf80812229cdd658bc8b 100644 GIT binary patch delta 30 lcmew;_)(C1IWI340}yO6+_I7TCJW0i#_C_2U$Ahp005S?3M~Kt delta 30 lcmew;_)(C1IWI340}%W=w0R@p9Y&jvvN diff --git a/ansible/lib/python3.11/site-packages/ansible/module_utils/_internal/_concurrent/__pycache__/_futures.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/module_utils/_internal/_concurrent/__pycache__/_futures.cpython-311.pyc index d93bd17417a01703e8c55d157aa65a1969d5df72..3a80b1548ce630782738300493dadfb429581770 100644 GIT binary patch delta 20 acmdnUy^))HIWI340}yO6+_I5-87lxb0tHV1 delta 20 acmdnUy^))HIWI340}%W=w0R@_ diff --git a/ansible/lib/python3.11/site-packages/ansible/module_utils/common/__pycache__/sys_info.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/module_utils/common/__pycache__/sys_info.cpython-311.pyc index 7c437e659359710a6e06e7c18c217d64b06dce69..9dd8e72777dad535d4e6c4fd0e5cb389663d39fa 100644 GIT binary patch delta 20 acmX@9aZ-bOIWI340}yO6+_I6|P6Plv delta 20 acmX@9aZ-bOIWI340}%W=w0R@9od^IzFa|aN diff --git a/ansible/lib/python3.11/site-packages/ansible/module_utils/common/__pycache__/validation.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/module_utils/common/__pycache__/validation.cpython-311.pyc index fcbc0826b12960f1e54b2c220228d91f3019daa5..9a427708a87171c0db228ea28f395e8a7d17b8cd 100644 GIT binary patch delta 29 kcmZ3tpK;xOM(*Xjyj%=Gu*Go8M(zhrjK4O&aXKUe0F|8zIWI340}yO6+_I7TJu3h~Y6dC* delta 20 acmeyx{fnD>IWI340}%W=w0R@c!807IWI340}yO6+%l2-C;&5h1#c!807IWI340}%W=w0R=;Q2;mZ21x(_ diff --git a/ansible/lib/python3.11/site-packages/ansible/module_utils/common/text/__pycache__/converters.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/module_utils/common/text/__pycache__/converters.cpython-311.pyc index b10499f6c35a94394e39345833e81796da26bc0d..8888f9cb8b122ad5e18e717db81b04fd8f54a290 100644 GIT binary patch delta 20 acmeyI|2dy~IWI340}yO6+_I7Tg*gCDlm`z0 delta 20 acmeyI|2dy~IWI340}%W=w0R@<3v&Qd-v{LY diff --git a/ansible/lib/python3.11/site-packages/ansible/module_utils/common/text/__pycache__/formatters.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/module_utils/common/text/__pycache__/formatters.cpython-311.pyc index 939fcddf7109ca91d7d73f9d4c3819e825a190f5..ba814385533476f30b0e4a9d032fcb52d7f1c1b0 100644 GIT binary patch delta 20 acmX@FcV3TsIWI340}yO6+_I6|MH~P=umynt delta 20 acmX@FcV3TsIWI340}%W=w0R@9i#Py7`vzA4 diff --git a/ansible/lib/python3.11/site-packages/ansible/module_utils/compat/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/module_utils/compat/__pycache__/__init__.cpython-311.pyc index a022d0f8c965ea900d99922c33ccec517a5d1a90..601d8949c2623c0c95fb9081761fa796c2ce2b3b 100644 GIT binary patch delta 19 ZcmX@jc$$%WIWI340}yO6+%l2-001(<1!MpK delta 19 ZcmX@jc$$%WIWI340}%W=w0R=;0RT5P208!$ diff --git a/ansible/lib/python3.11/site-packages/ansible/module_utils/compat/__pycache__/datetime.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/module_utils/compat/__pycache__/datetime.cpython-311.pyc index 010f7e5f4dbb60ef65044256d2b312d40e88a6ed..de0248872d13906483fca743df82bef71b929a8a 100644 GIT binary patch delta 20 acmeyw`-zu(IWI340}yO6+_I7TIU4{#a|Rax delta 20 acmeyw`-zu(IWI340}%W=w0R@W+XeRk diff --git a/ansible/lib/python3.11/site-packages/ansible/module_utils/distro/__pycache__/_distro.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/module_utils/distro/__pycache__/_distro.cpython-311.pyc index 1b3e22b85248b74b5c36d6c4884fa1975598d8e0..506db687522d56cf6221bb17bfd395ef2c098c11 100644 GIT binary patch delta 22 ccmX@Ni}~CxX71&@yj%=Gu*Go8MsDZ309XeHVgLXD delta 22 ccmX@Ni}~CxX71&@yj%=G@axd#joi+60bNoEH~;_u diff --git a/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/__pycache__/__init__.cpython-311.pyc index a96e608d03bd969711127cc0ae0e14af155a52fb..171c38318b673dadb45175dd467631f66351a2c1 100644 GIT binary patch delta 20 acmZo*ZeZqK&dbZi00dhMw`}AtWdr~%HUy{u delta 20 acmZo*ZeZqK&dbZi00h4dZQjUT$_M~5fdzg5 diff --git a/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/__pycache__/ansible_collector.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/__pycache__/ansible_collector.cpython-311.pyc index fd4408090075849c9e7fa81c692cd8f857013210..8bbd6504ee8990f0887e73f7ce39a02dc513993a 100644 GIT binary patch delta 20 acmaE;`%ssAIWI340}yO6+_I7Tniv2=TLv@$ delta 20 acmaE;`%ssAIWI340}%W=w0R@va$bacIWI340}yO6+_I6|g$n>W`vpJ% delta 20 acmX>va$bacIWI340}%W=w0R@93l{)DMg|rD diff --git a/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/__pycache__/default_collectors.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/__pycache__/default_collectors.cpython-311.pyc index 05a60f7e2b6b5649b9472eb605b8ade05e0ef405..c12f033016581fab0405ad664eb21525b174ee07 100644 GIT binary patch delta 20 acmaE6^2~&LIWI340}yO6+_I57S_%L|BL&p} delta 20 acmaE6^2~&LIWI340}%W=w0R?Uv=jhHZU(CW diff --git a/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/__pycache__/namespace.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/__pycache__/namespace.cpython-311.pyc index d26e2910c6bf9b5e9ca494eeb786019d07119890..70f27198784436e610a97739bac0a52b75c3c0d3 100644 GIT binary patch delta 20 acmdnRzl)!HIWI340}yO6+_I5-Jv#t5`~`FX delta 20 acmdnRzl)!HIWI340}%W=w0R@ia72K6IWI340}yO6+_I6|k^=xZu>}ia72K6IWI340}%W=w0R@9B?kaK`~~X( diff --git a/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/__pycache__/timeout.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/__pycache__/timeout.cpython-311.pyc index 86c4789c3f8956fc29a90a30c539edfa0c8f56b1..e3823a404ece7f230510ba1bbd136a4266cb3c39 100644 GIT binary patch delta 20 acmX>qbX16YIWI340}yO6+_I6|iW2}iA_W}) delta 20 acmX>qbX16YIWI340}%W=w0R@96(;~bZ3XiH diff --git a/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/__pycache__/utils.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/__pycache__/utils.cpython-311.pyc index f8751801b84a64dabb66aa12065b7f1c77c43a24..6c72586854acfbb0e848d1e02114202ef6f3070d 100644 GIT binary patch delta 20 acmZpaX_Vn!&dbZi00dhMw`}D8#tQ&3?geQ8 delta 20 acmZpaX_Vn!&dbZi00h4dZQjWJjTZnnIR-xf diff --git a/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/hardware/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/hardware/__pycache__/__init__.cpython-311.pyc index 1b4958d743e257f0daf626d19b287b69204cf63a..3cb25d0eba015e54c942c69e4445a7a9ccb5f02e 100644 GIT binary patch delta 19 Zcmcc2c$txVIWI340}yO6+%l2-1OPMX1$+Pi delta 19 Zcmcc2c$txVIWI340}%W=w0R=;2>>`=22ub3 diff --git a/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/hardware/__pycache__/aix.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/hardware/__pycache__/aix.cpython-311.pyc index a15852f8e0ec46f74eaced391e863b73cba45f20..ba5cab037248e44795762014c48887fa5687e8a9 100644 GIT binary patch delta 20 acmeyL_&tN$v&dbZi00dhMw`}D8%LD)~;{`_m delta 20 acmeBR>tN$v&dbZi00h4dZQjWJmk9thE(RR{ diff --git a/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/hardware/__pycache__/freebsd.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/hardware/__pycache__/freebsd.cpython-311.pyc index 02bb4fccd10ca119a0a3dc9286b12792ab9e7b1c..75d77f9b7450de933920117d9f4f8448e3c6ee59 100644 GIT binary patch delta 20 acmdmzyd{}?IWI340}yO6+_I5-r6B-A1_m?$ delta 20 acmdmzyd{}?IWI340}%W=w0R@iF-LOFBbz4Y%$!jk^9>s08WetRsaA1 delta 22 ccmZoX#?*L>iF-LOFBbz4{5rIGBlovO08`fpEC2ui diff --git a/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/hardware/__pycache__/netbsd.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/hardware/__pycache__/netbsd.cpython-311.pyc index 9c43b2331d5dd0e07cd648c8a9f41640974ac434..e99fbdb03f59c7161fb0f8881bb87b08f6f92ce7 100644 GIT binary patch delta 20 acmca*d&`!4IWI340}yO6+_I7Tyc_^Siv~vk delta 20 acmca*d&`!4IWI340}%W=w0R@ delta 20 acmca9bW@0XIWI340}%W=w0R@9A144ojRr3O diff --git a/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/network/__pycache__/darwin.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/network/__pycache__/darwin.cpython-311.pyc index 19a76cc04ebc1e0321352c4d340321d84fb5baea..11f32b15c8541c7ec0d07df19598ccee7dbe4f33 100644 GIT binary patch delta 20 acmeyt_k)jnIWI340}yO6+_I57mmL5=hy|{a0Yw; diff --git a/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/network/__pycache__/fc_wwn.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/network/__pycache__/fc_wwn.cpython-311.pyc index 3faf3c7c0a6ae8769a2eeb35357c2c25daa0f49f..c89e57ce02c316d6b24328ba870a2e38781f7662 100644 GIT binary patch delta 20 acmcbjdPS9cIWI340}yO6+_I7Tq!0i=T? delta 20 acmZ1_xk{3IIWI340}%W=w0R@#NiGzK#O diff --git a/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/network/__pycache__/iscsi.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/network/__pycache__/iscsi.cpython-311.pyc index 10443c6bdc0d4e61df5596f03bd07a22c5196564..41e4414dd24ac244f5ac80c2802b6f48d8012f2a 100644 GIT binary patch delta 20 acmbQJHBpOuIWI340}yO6+_I6IO%wn$ZUnsm delta 20 acmbQJHBpOuIWI340}%W=w0R>pn=NW&&dbZi00dhMw`}CD=KugO#RTa9 delta 20 acmeAY>=NW&&dbZi00h4dZQjUT&jA275Cy*g diff --git a/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/network/__pycache__/openbsd.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/network/__pycache__/openbsd.cpython-311.pyc index ca3ceb9f1c4c02866a728ee5db413110bd61904c..e0330922bfb9a22f3a3298e1b5e4658573001756 100644 GIT binary patch delta 20 acmbQtH<^!nIWI340}yO6+_I6IgB<`czyy5& delta 20 acmbQtH<^!nIWI340}%W=w0R>p2Ri^Z3k6dE diff --git a/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/network/__pycache__/sunos.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/network/__pycache__/sunos.cpython-311.pyc index 9c91b74970dc678fc414bbfa16daa4f7a678d39d..81cff349b94ab29f5bb309ff256f1bda44ae6930 100644 GIT binary patch delta 20 acmaE={#2cNIWI340}yO6+_I7TmM{Q9AOc!807IWI340}yO6+%l2-C;&5h1#c!807IWI340}%W=w0R=;Q2;mZ21x(_ diff --git a/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/other/__pycache__/facter.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/other/__pycache__/facter.cpython-311.pyc index 1c7eaed1aec82a853dbca0c2faa86cf50e18ec02..5280dfba26c9c2cc1a311aeb8877ebaadf738fd4 100644 GIT binary patch delta 20 acmcaDd0UcuIWI340}yO6+_I7T0uKN_kp=Jo delta 20 acmcaDd0UcuIWI340}%W=w0R@<1s(uH+y=$~ diff --git a/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/other/__pycache__/ohai.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/other/__pycache__/ohai.cpython-311.pyc index 98a335d18d1ebcd5a0c2c21e9513f795986d4d67..3d2156d194d6988dbb1ea2b56017b6d8dda7d8b4 100644 GIT binary patch delta 20 acmZ1|zEGTdIWI340}yO6+_I5-8aDtnI0Zcb delta 20 acmZ1|zEGTdIWI340}%W=w0R@p8!G@cDFs6S diff --git a/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/system/__pycache__/caps.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/system/__pycache__/caps.cpython-311.pyc index 4ea0fd434ae2cf292eaf005c92eee17d3627801f..c6407d6ce296e0711f080a452d4f8b37730bcab6 100644 GIT binary patch delta 20 acmca1dP9_ZIWI340}yO6+_I7TEEfPhhXvgL delta 20 acmca1dP9_ZIWI340}%W=w0R@nc}|jhIWI340}yO6+_I7TFb@Dci3Q03 delta 20 acmX>nc}|jhIWI340}%W=w0R@;}sK diff --git a/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/system/__pycache__/loadavg.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/system/__pycache__/loadavg.cpython-311.pyc index a5e41c855e6758d504e9361c461484743361b44f..9ec84acc06d0fc152ed000e0bb1a1108120168f6 100644 GIT binary patch delta 20 acmdnYwV8{1IWI340}yO6+_I5dgB1WZE(Fs6 delta 20 acmdnYwV8{1IWI340}%W=w0R@91}gwMc?GEe diff --git a/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/system/__pycache__/local.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/system/__pycache__/local.cpython-311.pyc index 2e644d9015a393c69987f7b510e7a61dc0cc58c5..79b0fceeb898347d69f7112fca802b931823c598 100644 GIT binary patch delta 20 acmdn0u~mb6IWI340}yO6+_I5dO9TKp#RVh) delta 20 acmdn0u~mb6IWI340}%W=w0R@9mIwep5C!@G diff --git a/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/system/__pycache__/lsb.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/system/__pycache__/lsb.cpython-311.pyc index ef27e828cf05dc0372e4ac0779b241594ab9eca6..12e101dd0dd61b41c994bc8b69f29816582ee41f 100644 GIT binary patch delta 20 acmZ1?yF`|IIWI340}yO6+_I5-CLaJciv?8x delta 20 acmZ1?yF`|IIWI340}%W=w0R@4F!k* delta 20 acmdllxL=TaIWI340}%W=w0R@<77hSDSO#7I diff --git a/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/system/__pycache__/selinux.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/system/__pycache__/selinux.cpython-311.pyc index ff17edfb1993fba5a17be1bb30c91bf2939ee89f..95ebd3bcf53ba0218e8f6ba16620e29720a10d1e 100644 GIT binary patch delta 20 acmbOxK24l^IWI340}yO6+_I6on;QT#fCU@? delta 20 acmbOxK24l^IWI340}%W=w0R?UH#Yz{%LVcP diff --git a/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/system/__pycache__/service_mgr.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/system/__pycache__/service_mgr.cpython-311.pyc index b223711ba4576925c4b3d570e147ed2201227717..63674ca50f22c997ee1389c3779bea66bfbe9369 100644 GIT binary patch delta 20 acmeyZ^IL~|IWI340}yO6+_I6oKnwsyM+N`@ delta 20 acmeyZ^IL~|IWI340}%W=w0R?UffxWvk_OfQ diff --git a/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/system/__pycache__/ssh_pub_keys.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/system/__pycache__/ssh_pub_keys.cpython-311.pyc index 10c2d1a16b9321a3a7e54f92dcdd26579d65b946..0c45a16dafde6e0b832c985b0e87c6ea66f07760 100644 GIT binary patch delta 20 acmaFC_kxdmIWI340}yO6+_I57mK^{-2nBoq delta 20 acmaFC_kxdmIWI340}%W=w0R?UEIR;0QwCB1 diff --git a/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/system/__pycache__/systemd.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/system/__pycache__/systemd.cpython-311.pyc index 261d61aa74d60602e8642455e3de4c365988fec9..02d892d7a8b06f5a8909c553ec49b9132cae1c05 100644 GIT binary patch delta 20 acmbQoH;<2dIWI340}yO6+_I5dm>mEzcm$pR delta 20 acmbQoH;<2dIWI340}%W=w0R@9FgpM@!v%Bz diff --git a/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/system/__pycache__/user.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/system/__pycache__/user.cpython-311.pyc index 3969c283e93abd2ba455dddbbcc063734f85e81f..1c07b1cd00e76410b4a6508d26d28e1bc2ca822b 100644 GIT binary patch delta 20 acmew(@JE1qIWI340}yO6+_I6okOKfgaRt2q delta 20 acmew(@JE1qIWI340}%W=w0R?UAqN0Pyatm1 diff --git a/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/virtual/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/virtual/__pycache__/__init__.cpython-311.pyc index 7523538fa98b2cacb8bf26cfe9a33e2a7d97a57f..eae44700f312e5b3f9dff00736b28c645694c197 100644 GIT binary patch delta 19 Zcmcb_c!`mFIWI340}yO6+%l2-H~=%m1$h7f delta 19 Zcmcb_c!`mFIWI340}%W=w0R=;aR4|u22TJ0 diff --git a/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/virtual/__pycache__/base.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/virtual/__pycache__/base.cpython-311.pyc index 38980a155b7858d9fc5786fa3080c85624ba02bc..a5ae1648d96454b97ea899c53e45aa9ee070466f 100644 GIT binary patch delta 20 acmdlkvR#CGIWI340}yO6+_I5dhYJ8VI|TRu delta 20 acmdlkvR#CGIWI340}%W=w0R@94i^ACh6T<5 diff --git a/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/virtual/__pycache__/dragonfly.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/virtual/__pycache__/dragonfly.cpython-311.pyc index d196517b81201da010bd252d25fd29edacd38ba8..874452d91e45a701649bc773eb3dd26fed6b606c 100644 GIT binary patch delta 20 acmX@idYF}aIWI340}yO6+_I5-2NM7{_XTzU delta 20 acmX@idYF}aIWI340}%W=w0R@<4kiFSLIz9# diff --git a/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/virtual/__pycache__/freebsd.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/module_utils/facts/virtual/__pycache__/freebsd.cpython-311.pyc index 7cb927913eb04b8901fd10f61692fb4ee50c356b..3cf02ae21e35ce7c1264b42efbbfa2416154aafb 100644 GIT binary patch delta 20 acmZ1>xk8eAIWI340}yO6+_I5-E)M`UnFUt> delta 20 acmZ1>xk8eAIWI340}%W=w0R@BzCxUPIWI340}yO6+_I5-E;j%+Pz6!| delta 20 acmZ1>zCxUPIWI340}%W=w0R@#;f20Z`( diff --git a/ansible/lib/python3.11/site-packages/ansible/module_utils/parsing/__pycache__/convert_bool.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/module_utils/parsing/__pycache__/convert_bool.cpython-311.pyc index d63a3a24e893840e50f04bf1f30d2d02324c9042..c60a846b068791c3ec2c7b10007b7a20de6f8a0e 100644 GIT binary patch delta 20 acmdnVzmuPPIWI340}yO6+_I5-9XkLw)CF+> delta 20 acmdnVzmuPPIWI340}%W=w0R@vChq0Dyj%=Gu*Go8M(&wg0a+sloB#j- delta 22 dcmZ4TnrX>vChq0Dyj%=G@axd#jodT00svla2yy@b diff --git a/ansible/lib/python3.11/site-packages/ansible/modules/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/modules/__pycache__/__init__.cpython-311.pyc index 335bf3f9ff05e502aa38c544209e1951fd3d9559..25e99774458b95d98b4c46c08b0d25dbd85b9402 100644 GIT binary patch delta 19 ZcmdnbxSx@GIWI340}yO6+%l1S3ji?%1wa4* delta 19 ZcmdnbxSx@GIWI340}%W=w0R=;763JR1{MGS diff --git a/ansible/lib/python3.11/site-packages/ansible/parsing/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/parsing/__pycache__/__init__.cpython-311.pyc index 0f0d2c1bb50d11511bbf74283acda320b747028b..e88c35b376e0c8babd17b4d165263ab92de2341a 100644 GIT binary patch delta 19 Zcmey%_?MA;IWI340}yO6+%l2-6975;1^NI0 delta 19 Zcmey%_?MA;IWI340}%W=w0R=;CjdTh2G9Ti diff --git a/ansible/lib/python3.11/site-packages/ansible/parsing/__pycache__/ajson.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/parsing/__pycache__/ajson.cpython-311.pyc index b1875d2b95fddc0cceda4815e5562aece51f045a..12a1bd718f4107a58a3e817129fb4fbff54f2991 100644 GIT binary patch delta 20 acmdlguvLJ2IWI340}yO6+_I5divs{QSp?_+ delta 20 acmdlguvLJ2IWI340}%W=w0R@976$-3qy@eJ diff --git a/ansible/lib/python3.11/site-packages/ansible/parsing/__pycache__/dataloader.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/parsing/__pycache__/dataloader.cpython-311.pyc index 4fb4e9847d5681dd543f3ca2e8e2ef93c0cd0518..074720a2cf8d47189f515dbecd706b65699721a7 100644 GIT binary patch delta 22 ccmbPxi*fEPM(*Xjyj%=Gu*Go8MsA^808{b?n*aa+ delta 22 ccmbPxi*fEPM(*Xjyj%=G@axd#jod=H09ic;aR2}S diff --git a/ansible/lib/python3.11/site-packages/ansible/parsing/__pycache__/mod_args.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/parsing/__pycache__/mod_args.cpython-311.pyc index bb776358a18b16ec0c1a3c2f4bf980d815036da0..467babf85a17922050a83a4834a2ed0b35c1d994 100644 GIT binary patch delta 20 acmeyK_dSn$IWI340}yO6+_I57#~c7o5C)k5 delta 20 acmeyK_dSn$IWI340}%W=w0R?UjyV8RTL*6d diff --git a/ansible/lib/python3.11/site-packages/ansible/parsing/__pycache__/plugin_docs.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/parsing/__pycache__/plugin_docs.cpython-311.pyc index 04ec88342420f7d6efea5e33aea471b3cc817683..56d99eb7332ccc063f2b7a113efe76cbe86dea09 100644 GIT binary patch delta 20 acmaDI^g4)pIWI340}yO6+_I57K@$K-G6pXI delta 20 acmaDI^g4)pIWI340}%W=w0R?Uf+he>eFp^q diff --git a/ansible/lib/python3.11/site-packages/ansible/parsing/__pycache__/quoting.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/parsing/__pycache__/quoting.cpython-311.pyc index 821eae65594ef27b06e39b1b8942623816a602cf..07b920a1201ad4ecd66984f969343777fbdbc399 100644 GIT binary patch delta 20 acmeyv_J@soIWI340}yO6+_I6okQo3#1O=o3 delta 20 acmeyv_J@soIWI340}%W=w0R?UAu|9)PX>Ab diff --git a/ansible/lib/python3.11/site-packages/ansible/parsing/__pycache__/splitter.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/parsing/__pycache__/splitter.cpython-311.pyc index 03b10c2b454df289301de3d9784f0a058b6288b5..eb0b98dd792af4e525757e9fcfe23314b79fa563 100644 GIT binary patch delta 20 acmdn)u-$=sIWI340}yO6+_I5dM*#pmfdys& delta 20 acmdn)u-$=sIWI340}%W=w0R@9jsgHe%mzFF diff --git a/ansible/lib/python3.11/site-packages/ansible/parsing/utils/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/parsing/utils/__pycache__/__init__.cpython-311.pyc index 3636e36eb79db37d536872b04b811b492cb2d099..20a615d6005bc707165cc9b99469bf3df3d2f7ba 100644 GIT binary patch delta 19 ZcmZo>YG&eI&dbZi00dhMw@l>z0RSlb1r7iJ delta 19 ZcmZo>YG&eI&dbZi00h4dZJx;e0{|^>1>^t# diff --git a/ansible/lib/python3.11/site-packages/ansible/parsing/utils/__pycache__/addresses.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/parsing/utils/__pycache__/addresses.cpython-311.pyc index d79dc78c5626382f5743a7ab0b13f2cb7d130681..ea4a21477cc22a28ac0a90197f27df7d76dda847 100644 GIT binary patch delta 20 acmbQHH%*UwIWI340}yO6+_I6ITO0s2$OP5^ delta 20 acmbQHH%*UwIWI340}%W=w0R>pw>SVh69udQ diff --git a/ansible/lib/python3.11/site-packages/ansible/parsing/utils/__pycache__/jsonify.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/parsing/utils/__pycache__/jsonify.cpython-311.pyc index 90fc7860d2f81452cbd59d594dade2bdf7eed4e5..1e92db7b6468ba3c36a5609149bb08a9beae0754 100644 GIT binary patch delta 20 acmcb{c8!gDIWI340}yO6+_I6|n;8H)6a_K> delta 20 acmcb{c8!gDIWI340}%W=w0R@9H!}b~Uj_&O diff --git a/ansible/lib/python3.11/site-packages/ansible/parsing/utils/__pycache__/yaml.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/parsing/utils/__pycache__/yaml.cpython-311.pyc index a3b2f6d8d4400af56bbe2a17b10a13b6cf1d8e0a..633c8ffb56e8479ade85321b9b63b25cda82bf43 100644 GIT binary patch delta 20 acmeB??UChP&dbZi00dhMw`}BYz4ge_D1q%QG delta 19 ZcmZo-YGUGE&dbZi00h4dZJx;e9RMv$1>pby diff --git a/ansible/lib/python3.11/site-packages/ansible/parsing/yaml/__pycache__/constructor.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/parsing/yaml/__pycache__/constructor.cpython-311.pyc index bf65eea6adde3ab321a063e29b89d99b9641f363..c1c86fa2455e23d10601b9408b011ca81b28bec3 100644 GIT binary patch delta 20 acmbPeJJFVVIWI340}yO6+_I6oO%4D!BL!># delta 20 acmbPeJJFVVIWI340}%W=w0R?Un;ZZ=ZU#aC diff --git a/ansible/lib/python3.11/site-packages/ansible/parsing/yaml/__pycache__/dumper.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/parsing/yaml/__pycache__/dumper.cpython-311.pyc index 254fe5891345661d4c1fef00c2d1d7e2a1750911..0bae0284d52af656ab93dbb7f634bb6fb1ce3f35 100644 GIT binary patch delta 20 acmZ3guvCG2IWI340}yO6+_I5dN&o;hAq3z6 delta 20 acmZ3guvCG2IWI340}%W=w0R@9lmGxbYz4Le diff --git a/ansible/lib/python3.11/site-packages/ansible/parsing/yaml/__pycache__/loader.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/parsing/yaml/__pycache__/loader.cpython-311.pyc index 9c4896d81ba1191c7edcb68b40707e200d78feab..c4c08c2e5ed52edcdc3ad02a01998895d0d7f3b7 100644 GIT binary patch delta 20 acmX>ia72K6IWI340}yO6+_I6|k^=xZu>}ia72K6IWI340}%W=w0R@9B?kaK`~~X( diff --git a/ansible/lib/python3.11/site-packages/ansible/parsing/yaml/__pycache__/objects.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/parsing/yaml/__pycache__/objects.cpython-311.pyc index 14318d1dd90ecaac4b1cd3631b16feb184d36051..701c3c57d1d4cf27e6a5974ee670a279e6027080 100644 GIT binary patch delta 22 ccmeygobl6gM(*Xjyj%=Gu*Go8M(*dq09#rIng9R* delta 22 ccmeygobl6gM(*Xjyj%=G@axd#joind`_5qIWI340}yO6+_I7TFed;zt_8XP delta 20 acmX>nd`_5qIWI340}%W=w0R@prvLyr#07%@ diff --git a/ansible/lib/python3.11/site-packages/ansible/playbook/__pycache__/handler_task_include.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/playbook/__pycache__/handler_task_include.cpython-311.pyc index 6a83a0cd8e2f966e18f9607a87ac80b663359099..983fba7bd11063948919f9fef5e03ae995892d84 100644 GIT binary patch delta 20 acmbQjHHC|NIWI340}yO6+_I6IlNA6kRs?nc delta 20 acmbQjHHC|NIWI340}%W=w0R>pCo2Fop#@9; diff --git a/ansible/lib/python3.11/site-packages/ansible/playbook/__pycache__/helpers.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/playbook/__pycache__/helpers.cpython-311.pyc index 90458a9fc29ea291d961a871fab118c05613d986..253c65c50ba8031d54a8146ff47fcada0cfa0544 100644 GIT binary patch delta 20 acmX?*bRda)IWI340}yO6+_I6|)DQqe!Ufg< delta 20 acmX?*bRda)IWI340}%W=w0R@9sUZMK4F;?L diff --git a/ansible/lib/python3.11/site-packages/ansible/playbook/__pycache__/included_file.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/playbook/__pycache__/included_file.cpython-311.pyc index 45c350a4b137a3f02c09e35010880b826d91459b..3317ebd5f208a3dd9826872f15d8db1c17b92a83 100644 GIT binary patch delta 20 acmdn*yWf|4IWI340}yO6+_I5-iy8nyn+6^L delta 20 acmdn*yWf|4IWI340}%W=w0R@<7Bv7y<_7ct diff --git a/ansible/lib/python3.11/site-packages/ansible/playbook/__pycache__/loop_control.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/playbook/__pycache__/loop_control.cpython-311.pyc index 0d177b542c9f78ea7a3055d630cb2e836fc9f055..a33b75253fdf96ca090d292405b2d14dcf8dc2d8 100644 GIT binary patch delta 20 acmdljxLc5WIWI340}yO6+_I5-0|x*&Z3TP) delta 20 acmdljxLc5WIWI340}%W=w0R@<1`Yr{xCT-H diff --git a/ansible/lib/python3.11/site-packages/ansible/playbook/__pycache__/notifiable.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/playbook/__pycache__/notifiable.cpython-311.pyc index 18df2d17e80160322cfb2bcf98ce570c337628f7..fc0cc21af459d31019bb82e8bca102e3c18fa8b1 100644 GIT binary patch delta 20 acmey)@|}fyIWI340}yO6+_I57hY0{a^97Iq delta 20 acmey)@|}fyIWI340}%W=w0R?U4if-GJ_cq0 diff --git a/ansible/lib/python3.11/site-packages/ansible/playbook/__pycache__/play.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/playbook/__pycache__/play.cpython-311.pyc index 99c53bfc4a65595c90ad7443f54319649d894d0a..b402143006d7f64a71f8c6d0d775da65f2ee8eae 100644 GIT binary patch delta 22 ccmbO*g>k|ZM(*Xjyj%=Gu*Go8Ms8Lw07mu&#Q*>R delta 22 ccmbO*g>k|ZM(*Xjyj%=G@axd#johqW08Bv!n*aa+ diff --git a/ansible/lib/python3.11/site-packages/ansible/playbook/__pycache__/play_context.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/playbook/__pycache__/play_context.cpython-311.pyc index b103350832d6372eb8be8f937a7410d3355d84bb..0f4a20b800ebbf0a1fc5fedb99e1370b061dd4ce 100644 GIT binary patch delta 20 acmZ1yyCjx-IWI340}yO6+_I5-rXB!4s|ED{ delta 20 acmZ1yyCjx-IWI340}%W=w0R@^-UeR) delta 20 acmccXeAk(KIWI340}%W=w0R@6GDK&dbZi00dhMw`}D8#|r>6E(L)A delta 20 acmeB_>6GDK&dbZi00h4dZQjWJj~4(qc?MSi diff --git a/ansible/lib/python3.11/site-packages/ansible/playbook/__pycache__/task.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/playbook/__pycache__/task.cpython-311.pyc index 8174e3bee7772bb2d93534098df5d8ac4b8190ef..7327e88a2bfe33cabd49524e5dca60d667abef88 100644 GIT binary patch delta 22 ccmbQUgK^FdM(*Xjyj%=Gu*Go8MsC3v08WSoLjV8( delta 22 ccmbQUgK^FdM(*Xjyj%=G@axd#jogAU08`Tk82|tP diff --git a/ansible/lib/python3.11/site-packages/ansible/playbook/__pycache__/task_include.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/playbook/__pycache__/task_include.cpython-311.pyc index 4e79211148e3ff47916b750ac0c13e31e57dc38d..b2256a5b220149fc851b72301d7f8bb5e241703e 100644 GIT binary patch delta 20 acmexo^v{TUIWI340}yO6+_I6oSP}q6ng$pE delta 20 acmexo^v{TUIWI340}%W=w0R?Uu_ORXua$JOaIWI340}yO6+_I6|h6?~Wy9Fo! delta 20 acmX>ua$JOaIWI340}%W=w0R@94Hp1E1_k~A diff --git a/ansible/lib/python3.11/site-packages/ansible/playbook/role/__pycache__/metadata.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/playbook/role/__pycache__/metadata.cpython-311.pyc index 4efa6e8d50a9d64f37dab7965054b45f1a6ea449..cef727ab69196c5697e9df560f75fdaeebcb832f 100644 GIT binary patch delta 20 acmeyM_d$<)IWI340}yO6+_I57RU80A*ahSO delta 20 acmeyM_d$<)IWI340}%W=w0R?UsyF~iBL=zv diff --git a/ansible/lib/python3.11/site-packages/ansible/playbook/role/__pycache__/requirement.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/playbook/role/__pycache__/requirement.cpython-311.pyc index 8cac5d097c21d1fafd9e837088f0560f89767a13..497f1c8294f937f9d1de5ef9de282a5e92b7105d 100644 GIT binary patch delta 20 acmeyZ@>_*_IWI340}yO6+_I6oKnMUtBL(aL delta 20 acmeyZ@>_*_IWI340}%W=w0R?Ufe-*mZU({t diff --git a/ansible/lib/python3.11/site-packages/ansible/plugins/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/plugins/__pycache__/__init__.cpython-311.pyc index cefbdd765fe7e62cdd97ca7868537d2e5f9807a6..3a1e013b2c612474e3b6b8759e19338b02dcfcb3 100644 GIT binary patch delta 20 acmccbeBYUSIWI340}yO6+_I7TiXs3-E(T=) delta 20 acmccbeBYUSIWI340}%W=w0R@<6-59_c?UZH diff --git a/ansible/lib/python3.11/site-packages/ansible/plugins/__pycache__/loader.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/plugins/__pycache__/loader.cpython-311.pyc index 5282678f3eb7d7a49e5eeff15305e4a26602b997..a676e41fbe1d2f69099bb566c761de8d4e5b7875 100644 GIT binary patch delta 25 fcmbRFie=_27VhP|yj%=Gu*GmoBllKrMm|#jYXb)k delta 25 fcmbRFie=_27VhP|yj%=G@axd#M((ZLjC`g5a?l6l diff --git a/ansible/lib/python3.11/site-packages/ansible/plugins/action/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/plugins/action/__pycache__/__init__.cpython-311.pyc index cb233d9d63c191aa0264f40d8868b295cdb9e7a0..fce8dd1d4e27d4be8073b004dbe1550fb6bcfed6 100644 GIT binary patch delta 22 ccmcceo%zalX71&@yj%=Gu*Go8M(&fp0b_Cqi~s-t delta 22 dcmcceo%zalX71&@yj%=G@axd#joc@H0|02f31R>M diff --git a/ansible/lib/python3.11/site-packages/ansible/plugins/action/__pycache__/command.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/plugins/action/__pycache__/command.cpython-311.pyc index 2e75581186f6223bd299323fe5d50f7f914010b8..07e7e1257d50901f794727d3d67bd6e4c01bdc64 100644 GIT binary patch delta 20 acmey({hOP6IWI340}yO6+_I7T11kVQk_IgR delta 20 acmey({hOP6IWI340}%W=w0R@<2UY+^-3J2z diff --git a/ansible/lib/python3.11/site-packages/ansible/plugins/action/__pycache__/copy.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/plugins/action/__pycache__/copy.cpython-311.pyc index 808b59a335feb194dcfeb7ce5bbae3d9cc813b44..72d5629356a2666ddd4a28f7f82cf00902def32b 100644 GIT binary patch delta 22 ccmX?lobl*!M(*Xjyj%=Gu*Go8M($n709d;RV*mgE delta 22 ccmX?lobl*!M(*Xjyj%=G@axd#joiDE0bT|OIRF3v diff --git a/ansible/lib/python3.11/site-packages/ansible/plugins/action/__pycache__/debug.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/plugins/action/__pycache__/debug.cpython-311.pyc index 1a7bd4aa03459a8e529315ffd889165b16018578..4aef0ab0603bce924b4f028751d004df18c96064 100644 GIT binary patch delta 20 acmeB@=#t=G&dbZi00dhMw`}D8&kX=F#07c) delta 20 acmeB@=#t=G&dbZi00h4dZQjWJpBn%;4+c;G diff --git a/ansible/lib/python3.11/site-packages/ansible/plugins/action/__pycache__/gather_facts.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/plugins/action/__pycache__/gather_facts.cpython-311.pyc index dbd22c6e9387743da2bd3ea9d172d81e010dd68c..282316a6a2ef8a00a3a660ced261755b8f56e940 100644 GIT binary patch delta 20 acmdlOv@wW#IWI340}yO6+_I5dO%nh=A_ai} delta 20 acmdlOv@wW#IWI340}%W=w0R@9nkE25Z3b5W diff --git a/ansible/lib/python3.11/site-packages/ansible/plugins/action/__pycache__/normal.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/plugins/action/__pycache__/normal.cpython-311.pyc index 83d46f5f38460042d37666b7fafc5e8eaa6ad844..52b5d70121772bd539d63970304a19e7469133e1 100644 GIT binary patch delta 20 acmZ3$yMUK_IWI340}yO6+_I5-DjNVZqXi)V delta 20 acmZ3$yMUK_IWI340}%W=w0R@?bo delta 20 acmca=aMgf&IWI340}%W=w0R@9mjnPsGzM+} diff --git a/ansible/lib/python3.11/site-packages/ansible/plugins/become/__pycache__/sudo.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/plugins/become/__pycache__/sudo.cpython-311.pyc index 57dcabeefd9d152394628d4e951f05cf45342f88..b000e8209c9fe37f849d9d46e300fcb34d9d52a7 100644 GIT binary patch delta 20 acmdm`vP*?~IWI340}yO6+_I5dUkCs>`voZg delta 20 acmdm`vP*?~IWI340}%W=w0R@9z7POEMg{)> diff --git a/ansible/lib/python3.11/site-packages/ansible/plugins/cache/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/plugins/cache/__pycache__/__init__.cpython-311.pyc index c752ab9f5f628b3d0911ffeecc77992d3650df20..52619420c5b9588f761fdf934b84a5d608c77894 100644 GIT binary patch delta 22 ccmZ3!igEcWM(*Xjyj%=Gu*Go8MsAr<08TgtK>z>% delta 22 ccmZ3!igEcWM(*Xjyj%=G@axd#jodP!08@hp7XSbN diff --git a/ansible/lib/python3.11/site-packages/ansible/plugins/cache/__pycache__/memory.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/plugins/cache/__pycache__/memory.cpython-311.pyc index 5f8091aa42823455d15931a6895d77b1f879204c..12a7a4a4ce8f15062aedbdc76ff4b97cb81f2429 100644 GIT binary patch delta 20 acmew%@%m+{%m+{i_@% diff --git a/ansible/lib/python3.11/site-packages/ansible/plugins/callback/__pycache__/minimal.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/plugins/callback/__pycache__/minimal.cpython-311.pyc index bd8244d96917dd7007778ffdd531f49559f030bf..187a9f58bd20cc6dd1c01950c99b681dfc17bbba 100644 GIT binary patch delta 20 acmbQQJ71T3IWI340}yO6+_I5-iWmSm3k7Zf delta 20 acmbQQJ71T3IWI340}%W=w0R@<6fpojRt7`> diff --git a/ansible/lib/python3.11/site-packages/ansible/plugins/callback/__pycache__/oneline.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/plugins/callback/__pycache__/oneline.cpython-311.pyc index f1f651ae42e55662b58c1d03e5419d0b1432200a..23f0995408082d60c02d8dc0fd7cac3a1c0ca64d 100644 GIT binary patch delta 20 acmcbqe^Z}(IWI340}yO6+_I7ToHzhMWCkGs delta 20 acmcbqe^Z}(IWI340}%W=w0R@t<8 delta 22 ccmeymkMZk1M(*Xjyj%=G@axd#joexB0AsTVGXMYp diff --git a/ansible/lib/python3.11/site-packages/ansible/plugins/connection/__pycache__/local.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/plugins/connection/__pycache__/local.cpython-311.pyc index 04438e743d5aec73a39b2aca2facd5e0b8fdac21..760c1cf797c823d3325b9eb13d082014ff0c69ab 100644 GIT binary patch delta 20 acmbQ6GB<^LIWI340}yO6+_I5d$Or&FlLc}B delta 20 acmbQ6GB<^LIWI340}%W=w0R@9kP!ew-Udhj diff --git a/ansible/lib/python3.11/site-packages/ansible/plugins/connection/__pycache__/paramiko_ssh.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/plugins/connection/__pycache__/paramiko_ssh.cpython-311.pyc index c9d8b61334e471c144f76a7e660ab924e6fac25d..f61f75ce0b69e32ca87f4d97beb35a688c225de1 100644 GIT binary patch delta 22 ccmX@r!E~yFiF-LOFBbz4Y%$!jk=won08c*#mjD0& delta 22 ccmX@r!E~yFiF-LOFBbz4{5rIGBe#7E091+xZ2$lO diff --git a/ansible/lib/python3.11/site-packages/ansible/plugins/connection/__pycache__/psrp.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/plugins/connection/__pycache__/psrp.cpython-311.pyc index d5e4354ea773657477c372926778731d59eae1ce..18b3590e978267aad7b79678786bd846ce4dd2c7 100644 GIT binary patch delta 22 ccmX@HjOoNOChq0Dyj%=Gu*Go8MsC~508@$v+yDRo delta 22 ccmX@HjOoNOChq0Dyj%=G@axd#joh}A0a(=svH$=8 diff --git a/ansible/lib/python3.11/site-packages/ansible/plugins/connection/__pycache__/ssh.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/plugins/connection/__pycache__/ssh.cpython-311.pyc index 0699d51db65572bba6f940743b8b42d4f0d24528..4eee68863553f97f880e7564e14b7becee35a511 100644 GIT binary patch delta 22 ccmdn{jCt=fX71&@yj%=Gu*Go8MsB0e09*D4od5s; delta 22 ccmdn{jCt=fX71&@yj%=G@axd#joe0`0bxN1a{vGU diff --git a/ansible/lib/python3.11/site-packages/ansible/plugins/connection/__pycache__/winrm.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/plugins/connection/__pycache__/winrm.cpython-311.pyc index 26421849b0001307dfcc01a78617949f9a7e23ce..fa1f8e51aa1027f37becf9dede4ea799956bbb5f 100644 GIT binary patch delta 22 ccmaFT!2G0vnR_`eFBbz4Y%$!jkvsAL08+*W6951J delta 22 ccmaFT!2G0vnR_`eFBbz4{5rIGBX{Hh09X+R=>Px# diff --git a/ansible/lib/python3.11/site-packages/ansible/plugins/doc_fragments/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/plugins/doc_fragments/__pycache__/__init__.cpython-311.pyc index 4c2309d4aadbd71f6e3b7fffb45e2942a6c1bc6e..76da58d84f518f655d41f502c03a06203f70a336 100644 GIT binary patch delta 19 ZcmX@hc$SfSIWI340}yO6+%l2-5CAg^1!@2Q delta 19 ZcmX@hc$SfSIWI340}%W=w0R=;Apkdk20#D+ diff --git a/ansible/lib/python3.11/site-packages/ansible/plugins/doc_fragments/__pycache__/connection_pipelining.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/plugins/doc_fragments/__pycache__/connection_pipelining.cpython-311.pyc index f3c82ca2fa090faf12946a75d194fc98e4b12f7e..b0e4e93b25dc8866b76a845d5c8474f585c67b7a 100644 GIT binary patch delta 20 acmdnXy_cJNIWI340}yO6+_I5-6Dt5W;stX6 delta 20 acmdnXy_cJNIWI340}%W=w0R@ delta 20 acmdldy-%8ZIWI340}%W=w0R@ucwCTsIWI340}yO6+_I5-4+j7`O$DF; delta 20 acmX>ucwCTsIWI340}%W=w0R@<9u5FMmIWI340}yO6+_I5-E(-uOQ3Wpm delta 20 acmZ3%xq_2>IWI340}%W=w0R@zCoRPIWI340}yO6+_I5-sW1RJ*aeaR delta 20 acmdm>zCoRPIWI340}%W=w0R@VIWI340}yO6+_I5-86N;RlLdSL delta 20 acmdleyHS>VIWI340}%W=w0R@> diff --git a/ansible/lib/python3.11/site-packages/ansible/plugins/shell/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/plugins/shell/__pycache__/__init__.cpython-311.pyc index d81e96d0dd30967ce0e13c63b130a19405bc9e99..0856b895c17efc5886948fee4f6b6536cb7ada6e 100644 GIT binary patch delta 20 acmcZ=eJh%KIWI340}yO6+_I7Tye*f${7FM(*Xjyj%=Gu*Go8M($&RoWB^Wi}Zo~Uz=|U7MlS8-)jsY delta 37 rcmX>*f${7FM(*Xjyj%=G@axd#joilsId3sm7wH4}w>IArEH(oG?!XMT diff --git a/ansible/lib/python3.11/site-packages/ansible/plugins/shell/__pycache__/sh.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/plugins/shell/__pycache__/sh.cpython-311.pyc index 5f12dc98d36c55a8c38e3d8a8e7cd23b86c20832..b716cbbd15346057b9a4ced013aeffca219ddfbb 100644 GIT binary patch delta 20 acmeAY?-J);&dbZi00dhMw`}CD=LP^Vpal2; delta 20 acmeAY?-J);&dbZi00h4dZQjUT&kX=J>jlmL diff --git a/ansible/lib/python3.11/site-packages/ansible/plugins/strategy/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/plugins/strategy/__pycache__/__init__.cpython-311.pyc index fdb1bd8746bac8f34861a5a8c234182b2ab325cd..3c590e1ae951517de1cdde9a41ecc96d54353a63 100644 GIT binary patch delta 22 ccmezKhWXDMX71&@yj%=Gu*Go8M()CI0B3jzX#fBK delta 22 ccmezKhWXDMX71&@yj%=G@axd#jogLb0BpkvKL7v# diff --git a/ansible/lib/python3.11/site-packages/ansible/plugins/strategy/__pycache__/linear.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/plugins/strategy/__pycache__/linear.cpython-311.pyc index 70d771824624cae6cd4a11446f7bc1703692103e..361d5395bf47e0a7123cf52c4a1aab6ed0577783 100644 GIT binary patch delta 22 ccmZpfz}PZ@k$X8WFBbz4Y%$!jk^84R07g>=mH+?% delta 22 ccmZpfz}PZ@k$X8WFBbz4{5rIGBlk~t085?+YybcN diff --git a/ansible/lib/python3.11/site-packages/ansible/plugins/test/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/plugins/test/__pycache__/__init__.cpython-311.pyc index f5566a4ec6ac648ff5b8c8b8ec698cb85d89f956..83df369867cf86bf04a5f17437d6f8a199e9834d 100644 GIT binary patch delta 20 acmZo=Z)N9R&dbZi00dhMw`}CDVg>*%kOaX1 delta 20 acmZo=Z)N9R&dbZi00h4dZQjUT#S8#5+Xa^Z diff --git a/ansible/lib/python3.11/site-packages/ansible/plugins/test/__pycache__/core.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/plugins/test/__pycache__/core.cpython-311.pyc index 8afab5b343a377826fc517e884b634fcbab1a293..cfd0906b240bc78872672345111a32552a34f33c 100644 GIT binary patch delta 20 acmeB4>`CNa&dbZi00dhMw`}BYGyniReFe7w delta 20 acmeB4>`CNa&dbZi00h4dZQjV;XaE2~$Oer7 diff --git a/ansible/lib/python3.11/site-packages/ansible/plugins/test/__pycache__/files.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/plugins/test/__pycache__/files.cpython-311.pyc index 982cc683a8018b2992949a52e33ee35827a9ecee..2312ce14d457cc57db7fce1f965b702b082ef292 100644 GIT binary patch delta 20 acmZqTYU1Kv&dbZi00dhMw`}D8&H?~0g#|wV delta 20 acmZqTYU1Kv&dbZi00h4dZQjWJodp0i&;}I% diff --git a/ansible/lib/python3.11/site-packages/ansible/plugins/test/__pycache__/mathstuff.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/plugins/test/__pycache__/mathstuff.cpython-311.pyc index 004ebc0e7593f600ab5677ae4027fb44d8e62d8a..26b93855c8f33127132298062654543ab36d1046 100644 GIT binary patch delta 20 acmey)`<<72IWI340}yO6+_I7T4I2PKI|e2I delta 20 acmey)`<<72IWI340}%W=w0R@<8#Vw&h6elq diff --git a/ansible/lib/python3.11/site-packages/ansible/plugins/test/__pycache__/uri.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/plugins/test/__pycache__/uri.cpython-311.pyc index fa6185bef12fc7f5a2c5ada93265d1b3b00cfcbc..3c3fe893ec9e6efecf3428b2d13b5709aa4f06cf 100644 GIT binary patch delta 20 acmaDU@KS(#IWI340}yO6+_I57jspNbc?E<3 delta 20 acmaDU@KS(#IWI340}%W=w0R?U90veH#0FXb diff --git a/ansible/lib/python3.11/site-packages/ansible/plugins/vars/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/plugins/vars/__pycache__/__init__.cpython-311.pyc index df63995f84c852cce299073ee651ce3f1d2db206..2174c7057e52627682407e4dad2fd67741d0b97c 100644 GIT binary patch delta 20 acmX@YeT18PIWI340}yO6+_I5-Co2Fr`UQvp delta 20 acmX@YeT18PIWI340}%W=w0R@t*7 diff --git a/ansible/lib/python3.11/site-packages/ansible/template/__pycache__/native_helpers.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/template/__pycache__/native_helpers.cpython-311.pyc index 7ee6c8e3e55424037788b755f735cfd1ba5d608c..2fa89664a22881956ba6a16200b036ad10810d27 100644 GIT binary patch delta 27 hcmX@=anyr*IWI340}yO6+_I5djF0ixW+gslF#vGQ2Y~

XWCa)i delta 20 acmeB^?~~_V&dbZi00h4dZQjV;%ntxKuLbS^ diff --git a/ansible/lib/python3.11/site-packages/ansible/utils/__pycache__/context_objects.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/utils/__pycache__/context_objects.cpython-311.pyc index c6e9ab9405dec81cfea53811a6b7a364c1a078dd..e6b3a341d0dba40fe02c00770436ca774c228325 100644 GIT binary patch delta 20 acmX@FdR~=#IWI340}yO6+_I7Th!6liX9em2 delta 20 acmX@FdR~=#IWI340}%W=w0R@<5g`CWvIf8a diff --git a/ansible/lib/python3.11/site-packages/ansible/utils/__pycache__/display.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/utils/__pycache__/display.cpython-311.pyc index 217159535c8cb0e1f81015176a21bb89a8f4c6ea..3fd62947d8e95fc1aeabb4768cf3ab3525c7eff6 100644 GIT binary patch delta 29 kcmX?cg6YHwChq0Dyj%=Gu*Go8M(*bpjJGy_wg}Az0Gs6s=l}o! delta 29 kcmX?cg6YHwChq0Dyj%=G@axd#joi;I7=La4Y!R9b0H?|e-T(jq diff --git a/ansible/lib/python3.11/site-packages/ansible/utils/__pycache__/encrypt.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/utils/__pycache__/encrypt.cpython-311.pyc index 587be4cc9a95b9cd34899fbaa12e44283c7d6c62..754c24950526b9f87d37af4f19cd718f8818171e 100644 GIT binary patch delta 20 acmdn(vD<@tIWI340}yO6+_I6|Km`ClMFoZc delta 20 acmdn(vD<@tIWI340}%W=w0R@9feHXbkOo`; diff --git a/ansible/lib/python3.11/site-packages/ansible/utils/__pycache__/fqcn.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/utils/__pycache__/fqcn.cpython-311.pyc index c5284a8652c48d1653b389d154a8830b84e14897..fd49fb0cad88219da43f00a3189966b6e13fb80e 100644 GIT binary patch delta 20 acmbQiK7*ZmIWI340}yO6+_I6oml*&thy?lo delta 20 acmbQiK7*ZmIWI340}%W=w0R?UFEao((*@7~ diff --git a/ansible/lib/python3.11/site-packages/ansible/utils/__pycache__/galaxy.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/utils/__pycache__/galaxy.cpython-311.pyc index 9f89851242398ca5fe05358716dcacf9d2415ffe..6fc8d33e92f0baac4189b181bb3f1dd1e5e9089d 100644 GIT binary patch delta 20 acmdm@wMC12IWI340}yO6+_I5dQxpI><^?7I delta 20 acmdm@wMC12IWI340}%W=w0R@9rYHbEF$Mep diff --git a/ansible/lib/python3.11/site-packages/ansible/utils/__pycache__/hashing.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/utils/__pycache__/hashing.cpython-311.pyc index 79c08170317dc4f06ed6836de627d9c98be3cd03..049dd8d7c540c68b2040fcb9d5b8c69308ce9638 100644 GIT binary patch delta 20 acmX>gen6aiIWI340}yO6+_I5-D>nc-5Cxh5 delta 20 acmX>gen6aiIWI340}%W=w0R@Jsk$X8WFBbz4Y%$!jk^8JW08>Z@5C8xG delta 22 ccmcc7&v>Jsk$X8WFBbz4{5rIGBllT%09ca;<^TWy diff --git a/ansible/lib/python3.11/site-packages/ansible/utils/__pycache__/sentinel.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/utils/__pycache__/sentinel.cpython-311.pyc index a677fb7fac9635b56e02857f15349b00b99fbdfe..515fd2e2f53af6240edc20cf71ce653be1550eef 100644 GIT binary patch delta 20 acmca0dO?(XIWI340}yO6+_I7TC>H=cM+L*3>G&dbZi00dhMw`}BQWCs8*s03&L delta 20 acmeC->*3>G&dbZi00h4dZQjVu$PNHB^94Qt diff --git a/ansible/lib/python3.11/site-packages/ansible/utils/__pycache__/ssh_functions.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/utils/__pycache__/ssh_functions.cpython-311.pyc index c65e8e315e7e6af63467193daf842ecaccc7c078..3bb40d09d63dd5d7684b983a8d622e1cc8082b6b 100644 GIT binary patch delta 20 acmeAX>=EQ%&dbZi00dhMw`}BY=EQ%&dbZi00h4dZQjV;$N>O0U40AQ2{=l}o! delta 22 ccmcchnDN$QM(*Xjyj%=G@axd#jojx80cGC^z5oCK diff --git a/ansible/lib/python3.11/site-packages/ansible/utils/__pycache__/vars.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/utils/__pycache__/vars.cpython-311.pyc index e62935625e9faf24d6268a1efd75e5e8dfc33142..9a3a3fcc49fbe24d61709eaec53f0edf870174a3 100644 GIT binary patch delta 27 hcmeBn?{?>2&dbZi00dhMw`}A#5@Y2&dbZi00h4dZQjUjB*u7avz?ePF92~z2r2*o diff --git a/ansible/lib/python3.11/site-packages/ansible/utils/__pycache__/version.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/utils/__pycache__/version.cpython-311.pyc index 48f58c5e62ad7565c05915d68cd10749106c09b8..e0dca215ea8c045ad2fe736da238090e8f2fad1a 100644 GIT binary patch delta 20 acmdmNMhB&dbZi00dhMw`}D8CjkI9Q3bdF delta 20 acmeA)>NMhB&dbZi00h4dZQjWJPXYitoCb~n diff --git a/ansible/lib/python3.11/site-packages/ansible/utils/collection_loader/__pycache__/_collection_finder.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/utils/collection_loader/__pycache__/_collection_finder.cpython-311.pyc index 16710637b8a1740c23bd4045c06f8af236bf7e1c..79bdeb354a19daf655d94969c1a2c2208ea0e7d6 100644 GIT binary patch delta 25 fcmaFe&homQg?l+KFBbz4Y%$!@$i0=D@c}acY8(fQ delta 25 fcmaFe&homQg?l+KFBbz4{5rI`k$WpQ;{#>@ap?$R diff --git a/ansible/lib/python3.11/site-packages/ansible/utils/collection_loader/__pycache__/_collection_meta.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/utils/collection_loader/__pycache__/_collection_meta.cpython-311.pyc index 9dea1e05872f745784d09b7ef6a70fcb4fad9c8f..514ffb2e133ce50cf7f3bb0010880b2bd45a5e3e 100644 GIT binary patch delta 20 acmeC+?%?KL&dbZi00dhMw`}CDWd#5)ngrAU delta 20 acmeC+?%?KL&dbZi00h4dZQjUT%L)KAfD delta 20 acmexr`_-0vIWI340}%W=w0R@(p74hL=k diff --git a/ansible/lib/python3.11/site-packages/ansible/vars/__pycache__/fact_cache.cpython-311.pyc b/ansible/lib/python3.11/site-packages/ansible/vars/__pycache__/fact_cache.cpython-311.pyc index 3dfba389bc619d9514c3ed9f89f5c0f874e4c596..9067d03d6bc613e34a6f150a6d1a6723c33b7a07 100644 GIT binary patch delta 20 acmdm?xI>Y9IWI340}yO6+_I5-tpETzJq4Qp delta 20 acmdm?xI>Y9IWI340}%W=w0R@=T? delta 20 acmZ1_xk{3IIWI340}%W=w0R@#NiGzK#O diff --git a/ansible/lib/python3.11/site-packages/cryptography/__pycache__/__about__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/cryptography/__pycache__/__about__.cpython-311.pyc index b4ffd6d132abf0d2503345b64494422e93246079..50145ed02d94c0b3483ddcb5cc1fb66aeb907145 100644 GIT binary patch delta 20 acmaFB{D7HzIWI340}$*o+_I7TDkA_qU&dbZi00g@Xw`}BY;syXRLIno^ delta 20 acmeAb?-l1>&dbZi00h4dZQjV;#0>y9V+Gd$ diff --git a/ansible/lib/python3.11/site-packages/cryptography/__pycache__/utils.cpython-311.pyc b/ansible/lib/python3.11/site-packages/cryptography/__pycache__/utils.cpython-311.pyc index 210d16073576eed9b868e26e4221584362b65bf9..672b064cb731f10f578c428cde1586bcfa8cba5b 100644 GIT binary patch delta 20 acmX?Xe%PFQIWI340}$*s+_I5-hco~`Nd@`< delta 20 acmX?Xe%PFQIWI340}%W=w0R@<4ru^HV+P3p diff --git a/ansible/lib/python3.11/site-packages/cryptography/hazmat/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/cryptography/hazmat/__pycache__/__init__.cpython-311.pyc index 2c52fa5202581b9d20b22063fc2f0e73b11dbeaa..08e97675b8bb42b7ca6b770ec7a86d91e4bea1ed 100644 GIT binary patch delta 19 ZcmZo=YGvYH&dbZi00g@Xw@l>z1pp~&1sVVV delta 19 ZcmZo=YGvYH&dbZi00h4dZJx;e3ji&;1?m6* diff --git a/ansible/lib/python3.11/site-packages/cryptography/hazmat/__pycache__/_oid.cpython-311.pyc b/ansible/lib/python3.11/site-packages/cryptography/hazmat/__pycache__/_oid.cpython-311.pyc index 45255a3a8b51c7e0f7c97decb62a1193e6b9cae3..e0e2f2e7dfe0211fc9ab872e774fe503e41bcbd4 100644 GIT binary patch delta 22 ccmZ2Hg>mr|M(*Xjyj%=Gu*-1EMs5i&07_8?1ONa4 delta 22 ccmZ2Hg>mr|M(*Xjyj%=G@axd#jocDm08erT)Bpeg diff --git a/ansible/lib/python3.11/site-packages/cryptography/hazmat/backends/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/cryptography/hazmat/backends/__pycache__/__init__.cpython-311.pyc index bf1cea14b365147a1e7345be4d99c289f79ce673..3da25f62f42e23afcb06ff86d1aba876a3118347 100644 GIT binary patch delta 20 acmdnZvYUl_IWI340}$*o+_I6|fC&IKy#(R_ delta 20 acmdnZvYUl_IWI340}%W=w0R@90TTc^-UYG% diff --git a/ansible/lib/python3.11/site-packages/cryptography/hazmat/backends/openssl/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/cryptography/hazmat/backends/openssl/__pycache__/__init__.cpython-311.pyc index 46f6fa9e66771875ab14d43c34c96d0876d3cfb3..60a2fa175914e985ee845ce4a086be2bfffad6ff 100644 GIT binary patch delta 20 acmZo?ZfE9R&dbZi00g@Xw`}CDVFUm!cm%@$ delta 20 acmZo?ZfE9R&dbZi00h4dZQjUT!w3K~nFW&o diff --git a/ansible/lib/python3.11/site-packages/cryptography/hazmat/backends/openssl/__pycache__/backend.cpython-311.pyc b/ansible/lib/python3.11/site-packages/cryptography/hazmat/backends/openssl/__pycache__/backend.cpython-311.pyc index 4d69e08718f8492943c86b83cf61be22d7af4b90..7875c461247e4275c5c70176c11aa92a24f26088 100644 GIT binary patch delta 20 acmZ3Vu|9))IWI340}$*o+_I5d#RLFC9|gDo delta 20 acmZ3Vu|9))IWI340}%W=w0R@9iU|NmKn92a diff --git a/ansible/lib/python3.11/site-packages/cryptography/hazmat/bindings/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/cryptography/hazmat/bindings/__pycache__/__init__.cpython-311.pyc index 3958ea0077f5c22f23b11c4b09a0717700957e81..f3b9cd2ea4ff10018083bd8ca133b78f571c9a41 100644 GIT binary patch delta 19 ZcmX@Zc!rUCIWI340}$*o+%l2-AOJH81#JKT delta 19 ZcmX@Zc!rUCIWI340}%W=w0R=;K>#;f20Z`( diff --git a/ansible/lib/python3.11/site-packages/cryptography/hazmat/bindings/openssl/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/cryptography/hazmat/bindings/openssl/__pycache__/__init__.cpython-311.pyc index 950b2dc7dfda8181b03015777d1cb7750e279fab..0f335a2cb1d39c735802e86fdd1e3f1f3bee83da 100644 GIT binary patch delta 19 Zcmcb@c!iOBIWI340}$*s+%l2-Bmgux1%?0s delta 19 Zcmcb@c!iOBIWI340}%W=w0R=;NdP#522}t6 diff --git a/ansible/lib/python3.11/site-packages/cryptography/hazmat/bindings/openssl/__pycache__/_conditional.cpython-311.pyc b/ansible/lib/python3.11/site-packages/cryptography/hazmat/bindings/openssl/__pycache__/_conditional.cpython-311.pyc index bf7dabc3043ac5e1729132a51b5a9c84e573467a..1a1cc96cdfe281929dc1411a8af280e80b158f9d 100644 GIT binary patch delta 20 acmbQCJwuy&IWI340}$*s+_I6oR}=s?+XY(y delta 20 acmbQCJwuy&IWI340}%W=w0R?UuP6XJ^#&>c diff --git a/ansible/lib/python3.11/site-packages/cryptography/hazmat/bindings/openssl/__pycache__/binding.cpython-311.pyc b/ansible/lib/python3.11/site-packages/cryptography/hazmat/bindings/openssl/__pycache__/binding.cpython-311.pyc index a3487ef4000868ea9539a2076b05019c6e2a3687..723cabe93efd56a82b6235dd7b58008c9987bd91 100644 GIT binary patch delta 20 acmaE){Yaa8IWI340}$*s+_I7Tx+nlbYX&(0 delta 20 acmaE){Yaa8IWI340}%W=w0R@SE$v&dbZi00g@Yw@l>z4*)7C1u6gl delta 19 ZcmeBT>SE$v&dbZi00h4dZJx;e9{?_M1^EB~ diff --git a/ansible/lib/python3.11/site-packages/cryptography/hazmat/decrepit/ciphers/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/cryptography/hazmat/decrepit/ciphers/__pycache__/__init__.cpython-311.pyc index cfe755524977af10e494a43d01655adcd17091ff..55ae49c6cfca42c1ba6b117d67d3e0229afd0b04 100644 GIT binary patch delta 20 acmbQlG>M6OIWI340}$*s+_I6Ioe=;p69i)b delta 20 acmbQlG>M6OIWI340}%W=w0R>pJ0k!zEd??F diff --git a/ansible/lib/python3.11/site-packages/cryptography/hazmat/decrepit/ciphers/__pycache__/algorithms.cpython-311.pyc b/ansible/lib/python3.11/site-packages/cryptography/hazmat/decrepit/ciphers/__pycache__/algorithms.cpython-311.pyc index 72ce03a38cbea0b6f197c25ef0f90103b643a0a7..310d65e33cff2218b2da1393ff40eb97de89c26b 100644 GIT binary patch delta 20 acmZ3cvrLD3IWI340}$*s+_I5dS_}XDE!lLfc{ diff --git a/ansible/lib/python3.11/site-packages/cryptography/hazmat/primitives/__pycache__/_cipheralgorithm.cpython-311.pyc b/ansible/lib/python3.11/site-packages/cryptography/hazmat/primitives/__pycache__/_cipheralgorithm.cpython-311.pyc index c5fc838bbd834bce3c2ca094a635a2b68bdb9d72..2da48980dfcf18538e6f2f170953ba4bb5d0c6db 100644 GIT binary patch delta 20 acmdlgx>b~WIWI340}$*s+_I5-6&Cb~WIWI340}%W=w0R@tf?x&dbZi00g@Yw`}D8&jbK4WCc_J delta 20 acmeBT>tf?x&dbZi00h4dZQjWJp9ugpeg-1| diff --git a/ansible/lib/python3.11/site-packages/cryptography/hazmat/primitives/__pycache__/hashes.cpython-311.pyc b/ansible/lib/python3.11/site-packages/cryptography/hazmat/primitives/__pycache__/hashes.cpython-311.pyc index c1737feef4d8fc59b4e5598d6a2e16dcd92b2d92..e02ae67aea7c1231a60632ea25856769010b7f42 100644 GIT binary patch delta 20 acmeD4>ht1W&dbZi00g@Yw`}BQRs{e!w*?CT delta 20 acmeD4>ht1W&dbZi00h4dZQjVutO@`<(FNK7 diff --git a/ansible/lib/python3.11/site-packages/cryptography/hazmat/primitives/__pycache__/hmac.cpython-311.pyc b/ansible/lib/python3.11/site-packages/cryptography/hazmat/primitives/__pycache__/hmac.cpython-311.pyc index 91e649f74f4ce7643bebd878fbf99f8e7620458a..74bdea93f342b56fac9e24a74aafcf0a0aded436 100644 GIT binary patch delta 20 acmaFJ@{omlIWI340}$*s+_I57j0pfcv;|cF delta 20 acmaFJ@{omlIWI340}%W=w0R?U7!v?M&ITj^ diff --git a/ansible/lib/python3.11/site-packages/cryptography/hazmat/primitives/__pycache__/padding.cpython-311.pyc b/ansible/lib/python3.11/site-packages/cryptography/hazmat/primitives/__pycache__/padding.cpython-311.pyc index b1d1ac883382c2e1d819cdeed8ad2c246365b328..0fcf5d3d8ee2b9e4a009014b5dc867cf7f177b28 100644 GIT binary patch delta 20 acmdntw8M#eIWI340}$*s+_I5dR}la{BL#H; delta 20 acmdntw8M#eIWI340}%W=w0R@9t|9^9u8k$bWW07%XTS^xk5 delta 22 ccmbQ&$vCf*k$X8WFBbz4{5rIGBllz%08QuzDF6Tf diff --git a/ansible/lib/python3.11/site-packages/cryptography/hazmat/primitives/asymmetric/__pycache__/padding.cpython-311.pyc b/ansible/lib/python3.11/site-packages/cryptography/hazmat/primitives/asymmetric/__pycache__/padding.cpython-311.pyc index 394442271047693c1d8ec2011096012e6f2b0245..dd977e2b59028154efdae67a974cead6f327d6e0 100644 GIT binary patch delta 20 acmcbqeN&rzIWI340}$*s+_I7ToG1W60|p)d delta 20 acmcbqeN&rzIWI340}%W=w0R@ delta 20 acmdmMw%3e%IWI340}%W=w0R@9ku(58PX;vr diff --git a/ansible/lib/python3.11/site-packages/cryptography/hazmat/primitives/ciphers/__pycache__/base.cpython-311.pyc b/ansible/lib/python3.11/site-packages/cryptography/hazmat/primitives/ciphers/__pycache__/base.cpython-311.pyc index eb1ab8eef663e8bbb35dae758ae4fc6c8b49c1e4..e9f189d37574b37f78311240e3a1a972dc19230b 100644 GIT binary patch delta 20 acmaE8`OuPkIWI340}$*s+_I7TnhXF$LIzm? delta 20 acmaE8`OuPkIWI340}%W=w0R@sa9DtQIWI340}$*s+_I6|f&&0Jx&;{k delta 20 acmX>sa9DtQIWI340}%W=w0R@91qT2<)CK4O diff --git a/ansible/lib/python3.11/site-packages/jinja2/__pycache__/_identifier.cpython-311.pyc b/ansible/lib/python3.11/site-packages/jinja2/__pycache__/_identifier.cpython-311.pyc index 7e7d471ab28b1f76377c31e568e3df9f115120c3..2fbc0cbb1f4265c542958e01899a1b3f1ab332bd 100644 GIT binary patch delta 20 acmcaCa9MzRIWI340}$*s+_I6|g989NIt57p delta 20 acmcaCa9MzRIWI340}%W=w0R@92L}K^R0bFT diff --git a/ansible/lib/python3.11/site-packages/jinja2/__pycache__/async_utils.cpython-311.pyc b/ansible/lib/python3.11/site-packages/jinja2/__pycache__/async_utils.cpython-311.pyc index 6d352b942a896686695551ec1b7aed43133e0da1..9b060e4824c35a7cc049af901b2a56075569fcfa 100644 GIT binary patch delta 20 acmcbveO;S-IWI340}$*s+_I7Tj3@v=vj!Ld delta 20 acmcbveO;S-IWI340}%W=w0R@<8BqX5%?9TH diff --git a/ansible/lib/python3.11/site-packages/jinja2/__pycache__/bccache.cpython-311.pyc b/ansible/lib/python3.11/site-packages/jinja2/__pycache__/bccache.cpython-311.pyc index 5b1e8bb3af43738a4ee17dd0ca23f8b0b2aad801..bc17639379b204748576df23ca3a09942335a576 100644 GIT binary patch delta 22 ccmdnEm~rD`M(*Xjyj%=Gu-kCUM($;S08j!3)Bpeg delta 22 ccmdnEm~rD`M(*Xjyj%=G@axd#joixu0aY9aqW}N^ diff --git a/ansible/lib/python3.11/site-packages/jinja2/__pycache__/compiler.cpython-311.pyc b/ansible/lib/python3.11/site-packages/jinja2/__pycache__/compiler.cpython-311.pyc index bd73116591a080ff541c0f84f9f37504e4a260ba..cef9ab257aa12c031bd578dfe5fd3f05166d0ce5 100644 GIT binary patch delta 40 vcmdn|j&1WhHtyxTyj%=Gu-kCUMsAOE#$TI5($8x#{%Tg4v|VKqBflyDD?AQp delta 40 vcmdn|j&1WhHtyxTyj%=G@axd#jocpTjJGz2q@UMfyw$8SX}iiKMt)TQJA@AR diff --git a/ansible/lib/python3.11/site-packages/jinja2/__pycache__/defaults.cpython-311.pyc b/ansible/lib/python3.11/site-packages/jinja2/__pycache__/defaults.cpython-311.pyc index 9f7ca5ed6f1b34b5f382d9ec0a2a15ae0afad223..11c177f2b851c135432f8148ae629381236f78a5 100644 GIT binary patch delta 20 acmdnOyM>o~IWI340}$*s+_I5-B^v-X^aW%9 delta 20 acmdnOyM>o~IWI340}%W=w0R@#jp@`hChq0Dyj%=Gu-kCUM(z$imS2q3zcx?j+gt+xtx5}s delta 32 ncmX>#jp@`hChq0Dyj%=G@axd#jocl4EVme|Z*88=x48xYxS9+X diff --git a/ansible/lib/python3.11/site-packages/jinja2/__pycache__/loaders.cpython-311.pyc b/ansible/lib/python3.11/site-packages/jinja2/__pycache__/loaders.cpython-311.pyc index 9abcc09d46c3a9465f164444e926879afe19eebc..3cffa1be9f43ea444bfbc85495e3814640f60ffb 100644 GIT binary patch delta 22 ccmccK!*soeiF-LOFBbz4>^9u8k=v&Y08%{$&j0`b delta 22 ccmccK!*soeiF-LOFBbz4{5rIGBezc*09RKBo&W#< diff --git a/ansible/lib/python3.11/site-packages/jinja2/__pycache__/nativetypes.cpython-311.pyc b/ansible/lib/python3.11/site-packages/jinja2/__pycache__/nativetypes.cpython-311.pyc index 54cb192f8c2a55e8a87c7da7a06d3c7182ed4a45..8d6e63aa8498baf401ce49b62b56b07f45147f1b 100644 GIT binary patch delta 20 acmbPgH`R`NIWI340}$*s+_I6IOCA6?83h6W delta 20 acmbPgH`R`NIWI340}%W=w0R>pmplMHGX>EA diff --git a/ansible/lib/python3.11/site-packages/jinja2/__pycache__/nodes.cpython-311.pyc b/ansible/lib/python3.11/site-packages/jinja2/__pycache__/nodes.cpython-311.pyc index 92246ed909067e8b32f5d8c6aa6700bcf0ac0839..8d4a8364c518ab092e0b4ff1f4dad3b154d4e437 100644 GIT binary patch delta 22 ccmccfo%zmpX71&@yj%=Gu-kCUM(&Hh0c86Ks{jB1 delta 22 dcmccfo%zmpX71&@yj%=G@axd#jocT10|06j33>nk diff --git a/ansible/lib/python3.11/site-packages/jinja2/__pycache__/optimizer.cpython-311.pyc b/ansible/lib/python3.11/site-packages/jinja2/__pycache__/optimizer.cpython-311.pyc index 27ef185460c3b25675e369daf86e7a80df124866..8ad272d3ee0685b52e759314811aaa4505d4e031 100644 GIT binary patch delta 20 acmbOyHcyOuIWI340}$*s+_I5dm>U2y%>=ms delta 20 acmbOyHcyOuIWI340}%W=w0R@9FgE}==LLuW diff --git a/ansible/lib/python3.11/site-packages/jinja2/__pycache__/parser.cpython-311.pyc b/ansible/lib/python3.11/site-packages/jinja2/__pycache__/parser.cpython-311.pyc index 34ecc57587460c665056c6a0445b5c4daa036f89..eee2eb59cf891e634b442075368dc4faf56909cf 100644 GIT binary patch delta 22 ccmex%h56eRX71&@yj%=Gu-kCUM(*ra0Alk9A^-pY delta 22 ccmex%h56eRX71&@yj%=G@axd#jojI<0B8*e@c;k- diff --git a/ansible/lib/python3.11/site-packages/jinja2/__pycache__/runtime.cpython-311.pyc b/ansible/lib/python3.11/site-packages/jinja2/__pycache__/runtime.cpython-311.pyc index c0d4b7f26c5f67499b972f095f29e21c0d7a0722..c3a3f262803c8c19d38b83c1d49271975bf738ea 100644 GIT binary patch delta 22 ccmZ45&%CssnR_`eFBbz4>^9u8k$cv0086q50ssI2 delta 22 ccmZ45&%CssnR_`eFBbz4{5rIGBloQ108q>a(EtDd diff --git a/ansible/lib/python3.11/site-packages/jinja2/__pycache__/tests.cpython-311.pyc b/ansible/lib/python3.11/site-packages/jinja2/__pycache__/tests.cpython-311.pyc index 382fb3e0850a6863c4d6ebc011f752b01873417e..c750437ce472cef4744c383872ed4102e95a9056 100644 GIT binary patch delta 20 acmdnwvB`sbIWI340}$*s+_I5dT?GI>4h3=m delta 20 acmdnwvB`sbIWI340}%W=w0R@9x(Wb8C^9u8k-IVl08UT_tN;K2 delta 22 ccmZoU$=Gs|k$X8WFBbz4{5rIGBX?yA08?rQdjJ3c diff --git a/ansible/lib/python3.11/site-packages/packaging/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/packaging/__pycache__/__init__.cpython-311.pyc index 75448a3c92122bd6849f56a13722143ee36c23ab..486aa44f938a7905f1f974a42c43184d7619724a 100644 GIT binary patch delta 20 acmZ3;vXF&)IWI340}$*s+_I5dj0pfSv;>s^ delta 20 acmZ3;vXF&)IWI340}%W=w0R@97!v?C&IM!u diff --git a/ansible/lib/python3.11/site-packages/packaging/__pycache__/_elffile.cpython-311.pyc b/ansible/lib/python3.11/site-packages/packaging/__pycache__/_elffile.cpython-311.pyc index 793309c561a0b91499c3957f0a9e203b7b46c0af..c45909b86a61cbea966c62fb8fc040f68bb8d828 100644 GIT binary patch delta 20 acmeyb^xj~bAIWI340}$*s+_I5-sR#f%QU#s> delta 20 acmdm>xj~bAIWI340}%W=w0R@^9u8k(((M08aY`N&o-= delta 22 ccmeA@#n^j_k$X8WFBbz4{5rIGBR5kj08|wR82|tP diff --git a/ansible/lib/python3.11/site-packages/packaging/__pycache__/utils.cpython-311.pyc b/ansible/lib/python3.11/site-packages/packaging/__pycache__/utils.cpython-311.pyc index 1161992f245fe640c040697c4a7dc0d72872e262..26a7966d994efaa18879bc043c3c09ad495906b6 100644 GIT binary patch delta 20 acmbPiJ=vOjIWI340}$*s+_I6oLlyuyTLp9g delta 20 acmbPiJ=vOjIWI340}%W=w0R?Uhb#a+bp}HK diff --git a/ansible/lib/python3.11/site-packages/packaging/__pycache__/version.cpython-311.pyc b/ansible/lib/python3.11/site-packages/packaging/__pycache__/version.cpython-311.pyc index 87f227dbc01dec256cf7c4f056bb2981cc3b1936..b482241b522fa76838b95c0b4cb99347f2700e33 100644 GIT binary patch delta 22 ccmX@Mn(^3bM(*Xjyj%=Gu-kCUM(*9A093^XCjbBd delta 22 ccmX@Mn(^3bM(*Xjyj%=G@axd#joiCK0a@P%_5c6? diff --git a/ansible/lib/python3.11/site-packages/passlib-1.7.4.dist-info/INSTALLER b/ansible/lib/python3.11/site-packages/passlib-1.7.4.dist-info/INSTALLER new file mode 100644 index 000000000..a1b589e38 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib-1.7.4.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/ansible/lib/python3.11/site-packages/passlib-1.7.4.dist-info/LICENSE b/ansible/lib/python3.11/site-packages/passlib-1.7.4.dist-info/LICENSE new file mode 100644 index 000000000..48c0f7208 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib-1.7.4.dist-info/LICENSE @@ -0,0 +1,116 @@ +.. -*- restructuredtext -*- + +===================== +Copyrights & Licenses +===================== + +Credits +======= +Passlib is primarily developed by Eli Collins. + +Special thanks to Darin Gordon for testing and +feedback on the :mod:`passlib.totp` module. + +License for Passlib +=================== +Passlib is (c) `Assurance Technologies `_, +and is released under the `BSD license `_:: + + Passlib + Copyright (c) 2008-2020 Assurance Technologies, LLC. + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the name of Assurance Technologies, nor the names of the + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Licenses for incorporated software +================================== +Passlib contains some code derived from the following sources: + +MD5-Crypt +--------- +The source file ``passlib/handlers/md5_crypt.py`` contains code derived from the original +`FreeBSD md5-crypt implementation `_, +which is available under the following license:: + + "THE BEER-WARE LICENSE" (Revision 42): + wrote this file. As long as you retain this notice you + can do whatever you want with this stuff. If we meet some day, and you think + this stuff is worth it, you can buy me a beer in return. Poul-Henning Kamp + + converted to python May 2008 + by Eli Collins + +DES +--- +The source file ``passlib/crypto/des.py`` contains code derived from +`UnixCrypt.java `_, +a pure-java implementation of the historic unix-crypt password hash algorithm. +It is available under the following license:: + + UnixCrypt.java 0.9 96/11/25 + Copyright (c) 1996 Aki Yoshida. All rights reserved. + Permission to use, copy, modify and distribute this software + for non-commercial or commercial purposes and without fee is + hereby granted provided that this copyright notice appears in + all copies. + + modified April 2001 + by Iris Van den Broeke, Daniel Deville + + modified Aug 2005 + by Greg Wilkins (gregw) + + converted to python Jun 2009 + by Eli Collins + +jBCrypt +------- +The source file ``passlib/crypto/_blowfish/base.py`` contains code derived +from `jBcrypt 0.2 `_, a Java +implementation of the BCrypt password hash algorithm. It is available under +a BSD/ISC license:: + + Copyright (c) 2006 Damien Miller + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTUOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +Wordsets +-------- +The EFF wordsets in ``passlib/_data/wordsets`` are (c) 2016 the Electronic Freedom Foundation. +They were downloaded from ``_, +and are released under the `Creative Commons License `_. diff --git a/ansible/lib/python3.11/site-packages/passlib-1.7.4.dist-info/METADATA b/ansible/lib/python3.11/site-packages/passlib-1.7.4.dist-info/METADATA new file mode 100644 index 000000000..0665d8af7 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib-1.7.4.dist-info/METADATA @@ -0,0 +1,40 @@ +Metadata-Version: 2.1 +Name: passlib +Version: 1.7.4 +Summary: comprehensive password hashing framework supporting over 30 schemes +Home-page: https://passlib.readthedocs.io +Author: Eli Collins +Author-email: elic@assurancetechnologies.com +License: BSD +Download-URL: https://pypi.python.org/packages/source/p/passlib/passlib-1.7.4.tar.gz +Keywords: password secret hash security +Provides-Extra: argon2 +Requires-Dist: argon2-cffi (>=18.2.0) ; extra == 'argon2' +Provides-Extra: bcrypt +Requires-Dist: bcrypt (>=3.1.0) ; extra == 'bcrypt' +Provides-Extra: build_docs +Requires-Dist: sphinx (>=1.6) ; extra == 'build_docs' +Requires-Dist: sphinxcontrib-fulltoc (>=1.2.0) ; extra == 'build_docs' +Requires-Dist: cloud-sptheme (>=1.10.1) ; extra == 'build_docs' +Provides-Extra: totp +Requires-Dist: cryptography ; extra == 'totp' + +Passlib is a password hashing library for Python 2 & 3, which provides +cross-platform implementations of over 30 password hashing algorithms, as well +as a framework for managing existing password hashes. It's designed to be useful +for a wide range of tasks, from verifying a hash found in /etc/shadow, to +providing full-strength password hashing for multi-user applications. + +* See the `documentation `_ + for details, installation instructions, and examples. + +* See the `homepage `_ + for the latest news and more information. + +* See the `changelog `_ + for a description of what's new in Passlib. + +All releases are signed with the gpg key +`4D8592DF4CE1ED31 `_. + + diff --git a/ansible/lib/python3.11/site-packages/passlib-1.7.4.dist-info/RECORD b/ansible/lib/python3.11/site-packages/passlib-1.7.4.dist-info/RECORD new file mode 100644 index 000000000..a3b5e3fcd --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib-1.7.4.dist-info/RECORD @@ -0,0 +1,202 @@ +passlib-1.7.4.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +passlib-1.7.4.dist-info/LICENSE,sha256=qVuo8a-I_41fDQwzUZ9DC3-diZK2nUvDaawEI6egWok,4954 +passlib-1.7.4.dist-info/METADATA,sha256=l-uRq14ie328RCoVsayT7AfMHaJqv34ICbpQtKG00jM,1688 +passlib-1.7.4.dist-info/RECORD,, +passlib-1.7.4.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +passlib-1.7.4.dist-info/WHEEL,sha256=ADKeyaGyKF5DwBNE0sRE5pvW-bSkFMJfBuhzZ3rceP4,110 +passlib-1.7.4.dist-info/top_level.txt,sha256=BA9xbJpLdaTxqvYbKigYnMQkzp8-UQr6S4m3lBTkxzw,8 +passlib-1.7.4.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1 +passlib/__init__.py,sha256=nSZrPEtlMQSKZqxERmYcWCDBD6pJ1P_DL5TdeSjIReU,87 +passlib/__pycache__/__init__.cpython-311.pyc,, +passlib/__pycache__/apache.cpython-311.pyc,, +passlib/__pycache__/apps.cpython-311.pyc,, +passlib/__pycache__/context.cpython-311.pyc,, +passlib/__pycache__/exc.cpython-311.pyc,, +passlib/__pycache__/hash.cpython-311.pyc,, +passlib/__pycache__/hosts.cpython-311.pyc,, +passlib/__pycache__/ifc.cpython-311.pyc,, +passlib/__pycache__/pwd.cpython-311.pyc,, +passlib/__pycache__/registry.cpython-311.pyc,, +passlib/__pycache__/totp.cpython-311.pyc,, +passlib/__pycache__/win32.cpython-311.pyc,, +passlib/_data/wordsets/bip39.txt,sha256=JM5Cwv1KlcG4a77pvOHhzyVb0AIuGbq2vVka_Wi379s,13117 +passlib/_data/wordsets/eff_long.txt,sha256=bVV_BpOVj7XmULaLW-5YXrgs9NoyllUFx4npJHQ7xSI,62144 +passlib/_data/wordsets/eff_prefixed.txt,sha256=eqV6TT7PZYFymZK62VdbrN6_fCg3ivKuxqUPEa7DJvU,10778 +passlib/_data/wordsets/eff_short.txt,sha256=NuzKSeT6IMqEsXbDLy6cgvmPRGWFGQ51-YealcCCR78,7180 +passlib/apache.py,sha256=TsHUCur5W8tK3Rsb9jYeeBCc7Ua_hP9e2tSxzoUVzwc,46661 +passlib/apps.py,sha256=AYqni3QIelR7HCiPj_hv2Mcr8bsfdcUkh07DwQqZxWs,8067 +passlib/context.py,sha256=aJeTjA-h7ke3KObvEM8aSJzKdN3wrOyu0hTt-MTbJt0,109195 +passlib/crypto/__init__.py,sha256=St6CGqhrfz3L5Da3aZvRK69le_FcLLE3gA2dEByOmC0,84 +passlib/crypto/__pycache__/__init__.cpython-311.pyc,, +passlib/crypto/__pycache__/_md4.cpython-311.pyc,, +passlib/crypto/__pycache__/des.cpython-311.pyc,, +passlib/crypto/__pycache__/digest.cpython-311.pyc,, +passlib/crypto/_blowfish/__init__.py,sha256=iZb7ft1vxBjCW7lpDtWwTxuMicgvi673M5F_1PKdVkg,6426 +passlib/crypto/_blowfish/__pycache__/__init__.cpython-311.pyc,, +passlib/crypto/_blowfish/__pycache__/_gen_files.cpython-311.pyc,, +passlib/crypto/_blowfish/__pycache__/base.cpython-311.pyc,, +passlib/crypto/_blowfish/__pycache__/unrolled.cpython-311.pyc,, +passlib/crypto/_blowfish/_gen_files.py,sha256=fUrNGWA5NX9CyvoJbNhJv7PJmptbp1uSR9iaWzKkb1I,6176 +passlib/crypto/_blowfish/base.py,sha256=_zF7x6XSbqCl2HH5Eya8KIhhJVbDYuYAWKfxbjOQZWg,20390 +passlib/crypto/_blowfish/unrolled.py,sha256=FOMhVo_jnGS3bMafXfjEffDPSP5vMogFvupnVKAa1lg,37153 +passlib/crypto/_md4.py,sha256=_5RXBX_gowtN0x05PnN0EF_csO4Q_NA5whm6e_vJx08,6905 +passlib/crypto/des.py,sha256=1EsvVd34Z82BYmGb8JIzfVWvTMN70fWhJGmIfmNrBAU,51878 +passlib/crypto/digest.py,sha256=WsfpcC8IM-gvZh56m6v8bjzG4nsNAsaoSv2LNY1_5go,36158 +passlib/crypto/scrypt/__init__.py,sha256=bXmeIerN6DKJSw8XsQEYcsUKCfRpXGb190e-gdHbbqU,9630 +passlib/crypto/scrypt/__pycache__/__init__.cpython-311.pyc,, +passlib/crypto/scrypt/__pycache__/_builtin.cpython-311.pyc,, +passlib/crypto/scrypt/__pycache__/_gen_files.cpython-311.pyc,, +passlib/crypto/scrypt/__pycache__/_salsa.cpython-311.pyc,, +passlib/crypto/scrypt/_builtin.py,sha256=82RZc_4LQv2JCL06bX70hCICBaK30Uy7PGzmZtiOjA0,8910 +passlib/crypto/scrypt/_gen_files.py,sha256=vRhjlIKqwvcILCo20sVf8dXr15tW636t5oojAZFssJE,4683 +passlib/crypto/scrypt/_salsa.py,sha256=b87_YEP3jJSmlU2BHSx-NKiJ4e_1eK-RlC4pWA4y71I,5719 +passlib/exc.py,sha256=MIjUTBLcOai52paDLM1nFh6lMTLBLPAn1PTdbCm-9Fo,14481 +passlib/ext/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1 +passlib/ext/__pycache__/__init__.cpython-311.pyc,, +passlib/ext/django/__init__.py,sha256=RvooHmuUwjLXuSuUJr-9URnY1CRVCzU2xdh-jW-mrN0,228 +passlib/ext/django/__pycache__/__init__.cpython-311.pyc,, +passlib/ext/django/__pycache__/models.cpython-311.pyc,, +passlib/ext/django/__pycache__/utils.cpython-311.pyc,, +passlib/ext/django/models.py,sha256=-XpQRLGG2kTuLWNoh-EhKOaeEV5aIfzavw8qTQ-p1fM,1314 +passlib/ext/django/utils.py,sha256=ObpILR1seOZyecYhuQ1G_R9_N6DMuS4kWZve_giRLiw,49409 +passlib/handlers/__init__.py,sha256=sIPjJgOGHpOIstAwDeHTfxKR8wLVqP4zSa4mvBhAZ_8,86 +passlib/handlers/__pycache__/__init__.cpython-311.pyc,, +passlib/handlers/__pycache__/argon2.cpython-311.pyc,, +passlib/handlers/__pycache__/bcrypt.cpython-311.pyc,, +passlib/handlers/__pycache__/cisco.cpython-311.pyc,, +passlib/handlers/__pycache__/des_crypt.cpython-311.pyc,, +passlib/handlers/__pycache__/digests.cpython-311.pyc,, +passlib/handlers/__pycache__/django.cpython-311.pyc,, +passlib/handlers/__pycache__/fshp.cpython-311.pyc,, +passlib/handlers/__pycache__/ldap_digests.cpython-311.pyc,, +passlib/handlers/__pycache__/md5_crypt.cpython-311.pyc,, +passlib/handlers/__pycache__/misc.cpython-311.pyc,, +passlib/handlers/__pycache__/mssql.cpython-311.pyc,, +passlib/handlers/__pycache__/mysql.cpython-311.pyc,, +passlib/handlers/__pycache__/oracle.cpython-311.pyc,, +passlib/handlers/__pycache__/pbkdf2.cpython-311.pyc,, +passlib/handlers/__pycache__/phpass.cpython-311.pyc,, +passlib/handlers/__pycache__/postgres.cpython-311.pyc,, +passlib/handlers/__pycache__/roundup.cpython-311.pyc,, +passlib/handlers/__pycache__/scram.cpython-311.pyc,, +passlib/handlers/__pycache__/scrypt.cpython-311.pyc,, +passlib/handlers/__pycache__/sha1_crypt.cpython-311.pyc,, +passlib/handlers/__pycache__/sha2_crypt.cpython-311.pyc,, +passlib/handlers/__pycache__/sun_md5_crypt.cpython-311.pyc,, +passlib/handlers/__pycache__/windows.cpython-311.pyc,, +passlib/handlers/argon2.py,sha256=XrMPknuG-16IAwrd7WUuTKdIkKOD-3UPlHHZOjXZe68,38934 +passlib/handlers/bcrypt.py,sha256=LF33HnoxOhjtr7aFtrKgU5SB4mtw3xGx7C4tqecosrk,53582 +passlib/handlers/cisco.py,sha256=Yz0KhmqVVAV_szNnuZq40WgYg6eomBRoAJBbRrSUkGg,16284 +passlib/handlers/des_crypt.py,sha256=W3srE5kIaRQdhfIObz237sm0vPgqR4p_9ZkSd-9UNPo,22367 +passlib/handlers/digests.py,sha256=AeuVSxas2793ILXX0s6xm1lA1u4RPpE9G8wZSaq0Bs4,6327 +passlib/handlers/django.py,sha256=MmoLua6kZWVItsjRrnDgfktzuEpqlvwlPaexvti6I9M,20185 +passlib/handlers/fshp.py,sha256=78sMdnAkW5YHCPC13bLptdElLFrWzZF7rm3bwUWHATo,7799 +passlib/handlers/ldap_digests.py,sha256=jgxtxcERep4xXgVVfKfSVe5JEE45b5tt87NKGvK9_Zk,13049 +passlib/handlers/md5_crypt.py,sha256=jLt3IP-l0HFfU1u2VEtGI1WBYVNjTqhjvwovfFREiwg,13740 +passlib/handlers/misc.py,sha256=o1tWKAdTp3EnCYJOERpdkQnRwrQfWWKeiJXSQurbVMo,10109 +passlib/handlers/mssql.py,sha256=BECU0VaVtc-RzhGx7E_LVu2moZpEI5GASChFJnzDVxA,8482 +passlib/handlers/mysql.py,sha256=8h83lpTHs5q8zflXP0TyMavrELgtlvgUbcLtFUHnbDY,4796 +passlib/handlers/oracle.py,sha256=WDCqJEo2rDihcuUs4Ka48JBpSm4_JnNqXIVsCGUrkO8,6691 +passlib/handlers/pbkdf2.py,sha256=jVqdo1MSD3_7B5m-osqUTBTwTXnhedLan9lQaM-gysU,19010 +passlib/handlers/phpass.py,sha256=0c7maDUNGxIuyiG_O2hK6MJbxcTu7V2vx67xOq8d7ps,4785 +passlib/handlers/postgres.py,sha256=y9AzGpxjK-z1HLHElRQtLzCMqqtvBwd_xxJraHdGpN4,2274 +passlib/handlers/roundup.py,sha256=lvYArKerC702_MHZXMi3F-iZ9Y2jH10h2UXKDpgqoO8,1178 +passlib/handlers/scram.py,sha256=wBsoBg0qLW8HA5Nsgcnd1bM7ZDYEFbapAGoP0_44N58,22539 +passlib/handlers/scrypt.py,sha256=OYfF2Jjltydr5BswyZ-uFgl4yEjQZowGdIZpEyB7s5Q,14146 +passlib/handlers/sha1_crypt.py,sha256=DZOdKExzlucHCfpgszG1cFdareTqpGUGORNIEn4FJCs,5873 +passlib/handlers/sha2_crypt.py,sha256=kTZm-jmRVnKRhquetVBbiDWi9eY87NTJvUYkjGEm7MY,21800 +passlib/handlers/sun_md5_crypt.py,sha256=uWhoKxBITVwPlh9MIQ3WjVrYjlRMgLrBjLR1Ui2kmZw,13933 +passlib/handlers/windows.py,sha256=nviGebFjOiJO_cDJRo7RiccEhlN2UM7nAQL0pTso9MQ,12384 +passlib/hash.py,sha256=9lVasGFiXDGcL8VOWuEwAjzlATQbmEYF30wOIVotP-U,3750 +passlib/hosts.py,sha256=odRo2WnSfjMuktSIwfR50rzxbKGfzUwZ2CUkvcxvJoA,3302 +passlib/ifc.py,sha256=kL2svtkF99VQDOim_6TE6OGhmSf2EyHrzp0v_UQksqA,14196 +passlib/pwd.py,sha256=VeU_PVkZSvwXPI6AQA96cjqIKyuTvXtUoCK7eI5ab7w,28690 +passlib/registry.py,sha256=5qLDF72XHGSQVoEVqhvEngfZsO2fxVsBpWntX_D0YRs,20301 +passlib/tests/__init__.py,sha256=JIK29mBP8OKz3ChmaEbyr9vvml3weGe7YHMTHzBJcr0,20 +passlib/tests/__main__.py,sha256=iKv9ZuQe5jBzp4Gyp_G3wXhQBxSTJguMx1BCCVVZL6Y,82 +passlib/tests/__pycache__/__init__.cpython-311.pyc,, +passlib/tests/__pycache__/__main__.cpython-311.pyc,, +passlib/tests/__pycache__/_test_bad_register.cpython-311.pyc,, +passlib/tests/__pycache__/backports.cpython-311.pyc,, +passlib/tests/__pycache__/test_apache.cpython-311.pyc,, +passlib/tests/__pycache__/test_apps.cpython-311.pyc,, +passlib/tests/__pycache__/test_context.cpython-311.pyc,, +passlib/tests/__pycache__/test_context_deprecated.cpython-311.pyc,, +passlib/tests/__pycache__/test_crypto_builtin_md4.cpython-311.pyc,, +passlib/tests/__pycache__/test_crypto_des.cpython-311.pyc,, +passlib/tests/__pycache__/test_crypto_digest.cpython-311.pyc,, +passlib/tests/__pycache__/test_crypto_scrypt.cpython-311.pyc,, +passlib/tests/__pycache__/test_ext_django.cpython-311.pyc,, +passlib/tests/__pycache__/test_ext_django_source.cpython-311.pyc,, +passlib/tests/__pycache__/test_handlers.cpython-311.pyc,, +passlib/tests/__pycache__/test_handlers_argon2.cpython-311.pyc,, +passlib/tests/__pycache__/test_handlers_bcrypt.cpython-311.pyc,, +passlib/tests/__pycache__/test_handlers_cisco.cpython-311.pyc,, +passlib/tests/__pycache__/test_handlers_django.cpython-311.pyc,, +passlib/tests/__pycache__/test_handlers_pbkdf2.cpython-311.pyc,, +passlib/tests/__pycache__/test_handlers_scrypt.cpython-311.pyc,, +passlib/tests/__pycache__/test_hosts.cpython-311.pyc,, +passlib/tests/__pycache__/test_pwd.cpython-311.pyc,, +passlib/tests/__pycache__/test_registry.cpython-311.pyc,, +passlib/tests/__pycache__/test_totp.cpython-311.pyc,, +passlib/tests/__pycache__/test_utils.cpython-311.pyc,, +passlib/tests/__pycache__/test_utils_handlers.cpython-311.pyc,, +passlib/tests/__pycache__/test_utils_md4.cpython-311.pyc,, +passlib/tests/__pycache__/test_utils_pbkdf2.cpython-311.pyc,, +passlib/tests/__pycache__/test_win32.cpython-311.pyc,, +passlib/tests/__pycache__/tox_support.cpython-311.pyc,, +passlib/tests/__pycache__/utils.cpython-311.pyc,, +passlib/tests/_test_bad_register.py,sha256=yws8uO2HsUWg8GRQPlxKvE5HniP84QSQW6ncCPiZDpw,541 +passlib/tests/backports.py,sha256=QTi9tD9DO_RlawkInpPDsFaol--5hsMI-cFvwLIE9B0,2593 +passlib/tests/sample1.cfg,sha256=lJsayArbi6FElINzcTQ1VbgTTGY5LKpMdbCJvK_6H8s,243 +passlib/tests/sample1b.cfg,sha256=2ZQnnpumQsEJpKFsTOHuv_ULhQY5PhQPnsa2rSZmTEU,252 +passlib/tests/sample1c.cfg,sha256=u-BGMklAN05efndzADJfFV9gP1Jbns1gDdwC__VfW-8,490 +passlib/tests/sample_config_1s.cfg,sha256=mMgYjX_UvxVVLFTfZ4m-vxVo31MbSNrZA0R7VY6DzTk,238 +passlib/tests/test_apache.py,sha256=_XhDKgV1nON4ddQQU3GdUfSXrwY_x2OoJQ6l7w2Gzbw,29432 +passlib/tests/test_apps.py,sha256=6MrGeFenjSACzbAtp6jf3PNHoITv_v5DbT_7nhrR-KA,5281 +passlib/tests/test_context.py,sha256=Vsl2hhouEi3yn4_J7J10E09OotLneRHzkAY_jS16F08,74546 +passlib/tests/test_context_deprecated.py,sha256=cVXqcPx_Xqlsh6QF2az34RY23wP3pv8SOBbJFQn65Jg,29282 +passlib/tests/test_crypto_builtin_md4.py,sha256=5PWKh1HoQKC4gI4BcgVDh89xw7lix0R1n9Jn0Y8t8mQ,5660 +passlib/tests/test_crypto_des.py,sha256=0xWgS74G6ygl7gIvF6uhjcoThVTt1TqIH4ZUeqXbVmA,8874 +passlib/tests/test_crypto_digest.py,sha256=b15XIFLDUsjsaxPEQUJkb-csM65IRz_9glwZz7qwN7U,20478 +passlib/tests/test_crypto_scrypt.py,sha256=xJDU3e4bt9N1X0fA9zBLBxESk3PsTR89qJeEWNX2Em4,26646 +passlib/tests/test_ext_django.py,sha256=QUKoa6rLn3hbCVNk7_0z9JW5aOFmyLbBwj0PiWhQJ7s,41364 +passlib/tests/test_ext_django_source.py,sha256=AW-PQRQeLz2cOpKGPeKPLSESC4o-ATbu3-Zd45Coi3k,11034 +passlib/tests/test_handlers.py,sha256=WxYhRTthTzDj-FIP2vS_mH0nlpjgrWOp2C-h3mN6DzE,68622 +passlib/tests/test_handlers_argon2.py,sha256=bSNARahGKPZTawLq-qhVdcuvprCDTNXGWPhSh8aRyaY,22837 +passlib/tests/test_handlers_bcrypt.py,sha256=izOVd0WthIi90YKkvskrW5DZPMMCvO2qtwRkefvgkdY,29549 +passlib/tests/test_handlers_cisco.py,sha256=TLvuGQZygEZbjA01t1hfGfBvx3THnv6ZwbNQCKUhsuI,20471 +passlib/tests/test_handlers_django.py,sha256=ADphUgbG9PwoXQPFbEAPeIDfqjK6DENl_wizP52wYSE,15538 +passlib/tests/test_handlers_pbkdf2.py,sha256=vDM9ipts9EYoauheNHtOOYq0Nl8-9ltTML4gnw2EB2g,18788 +passlib/tests/test_handlers_scrypt.py,sha256=wHsbgoV5xhY4SQtgWFCuit3lygkNvd0AQKZ0lmp72do,4188 +passlib/tests/test_hosts.py,sha256=n0gCywmbsw8q8p4WLp-AlQrQuPfe-29fYwUfWwXi4Co,3906 +passlib/tests/test_pwd.py,sha256=Si9qFDXwkbjTsJ9wQTYe-QhlprVoMQ2E79-eX11FPBk,7190 +passlib/tests/test_registry.py,sha256=9BgXvMhHKQQHBGdgV4WyDDZUboUh0tbHYdgPYr1upSo,9246 +passlib/tests/test_totp.py,sha256=T1o3B97SltvC1OKweXQpX1bBGf6KYQnMl8jcpBSg5DU,65746 +passlib/tests/test_utils.py,sha256=yMWrrnsMIg8b8guyzRK8lDJ243rul6ANhrIgImGlyVI,46118 +passlib/tests/test_utils_handlers.py,sha256=rVSuaNqRUb4Q520nVD4C5smzVs-LdFqQjFZMDRTz-zU,32134 +passlib/tests/test_utils_md4.py,sha256=CfQor3ZfV2JO_8x2RxY5Tl5ZsS0hDvIje46cLvLN5Ew,1474 +passlib/tests/test_utils_pbkdf2.py,sha256=gIhycQf4NUNd5yjUrtKfRm3eqqpklS9W2B7-8INp4Cg,12193 +passlib/tests/test_win32.py,sha256=BXVpHSm71ePXmmbBPTN4H38lUgGqG6-iZasbj_l1mVg,1920 +passlib/tests/tox_support.py,sha256=PDaO1ftDtOFzd299EXm0X5HWRzg37VsBiHsdiMOu5FA,2473 +passlib/tests/utils.py,sha256=mNbhjFNG16dmU13ChMyqOSY39OiR2d8LRUBi41dAMko,147541 +passlib/totp.py,sha256=Wryr57req8NFJnw1fI_eycCaTwmSY8WA7Z3OFjAwHOE,73033 +passlib/utils/__init__.py,sha256=VHkQHu7DcdVKyDjhPuyRG_2-25aI4Zwat3wr6K-rAlo,42925 +passlib/utils/__pycache__/__init__.cpython-311.pyc,, +passlib/utils/__pycache__/binary.cpython-311.pyc,, +passlib/utils/__pycache__/decor.cpython-311.pyc,, +passlib/utils/__pycache__/des.cpython-311.pyc,, +passlib/utils/__pycache__/handlers.cpython-311.pyc,, +passlib/utils/__pycache__/md4.cpython-311.pyc,, +passlib/utils/__pycache__/pbkdf2.cpython-311.pyc,, +passlib/utils/binary.py,sha256=dZe2ZjuGr0g6iQseO-ThkQ5XM6KnQFISGQr68vUOOhM,31422 +passlib/utils/compat/__init__.py,sha256=xuPP5PsmLJh_I5NrlaYa012zmWrdzfrYbL_oHqc4tCk,14235 +passlib/utils/compat/__pycache__/__init__.cpython-311.pyc,, +passlib/utils/compat/__pycache__/_ordered_dict.cpython-311.pyc,, +passlib/utils/compat/_ordered_dict.py,sha256=1nga6blaxokrrDdY3UrQgRXYdifZHCDgPYie1aCJkuI,8368 +passlib/utils/decor.py,sha256=svc2C-_DKfiCMmOBNhn_DK7IeS_WYNg26asjhx76LUA,7651 +passlib/utils/des.py,sha256=jFuvhUA3aaiR1xWX4NpXYm5XgcdewRT5Uas-7jLoSTE,2163 +passlib/utils/handlers.py,sha256=E3oRL908uudK_ZLZWeX5DoPxJL8uCfCGpmAkyfJoWQ8,105286 +passlib/utils/md4.py,sha256=pyxEpUe_t8E0u2ZDWOzYIJa0oXgTQBO7DQ8SMKGX8ag,1218 +passlib/utils/pbkdf2.py,sha256=foDGTAKeZywBAVlLZIRf4bX6fC3bzsoC1i_DtcdXr2I,6832 +passlib/win32.py,sha256=E6Ca-4Ki5ZlCSzd86N1CXjh-xQoJYjW-74-kJ6VsHUU,2591 diff --git a/ansible/lib/python3.11/site-packages/passlib-1.7.4.dist-info/REQUESTED b/ansible/lib/python3.11/site-packages/passlib-1.7.4.dist-info/REQUESTED new file mode 100644 index 000000000..e69de29bb diff --git a/ansible/lib/python3.11/site-packages/passlib-1.7.4.dist-info/WHEEL b/ansible/lib/python3.11/site-packages/passlib-1.7.4.dist-info/WHEEL new file mode 100644 index 000000000..6d38aa060 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib-1.7.4.dist-info/WHEEL @@ -0,0 +1,6 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.35.1) +Root-Is-Purelib: true +Tag: py2-none-any +Tag: py3-none-any + diff --git a/ansible/lib/python3.11/site-packages/passlib-1.7.4.dist-info/top_level.txt b/ansible/lib/python3.11/site-packages/passlib-1.7.4.dist-info/top_level.txt new file mode 100644 index 000000000..419829dd6 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib-1.7.4.dist-info/top_level.txt @@ -0,0 +1 @@ +passlib diff --git a/ansible/lib/python3.11/site-packages/passlib-1.7.4.dist-info/zip-safe b/ansible/lib/python3.11/site-packages/passlib-1.7.4.dist-info/zip-safe new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib-1.7.4.dist-info/zip-safe @@ -0,0 +1 @@ + diff --git a/ansible/lib/python3.11/site-packages/passlib/__init__.py b/ansible/lib/python3.11/site-packages/passlib/__init__.py new file mode 100644 index 000000000..963bfcc9c --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/__init__.py @@ -0,0 +1,3 @@ +"""passlib - suite of password hashing & generation routines""" + +__version__ = '1.7.4' diff --git a/ansible/lib/python3.11/site-packages/passlib/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..21ab2c8e4386f8cbc2aad1a58d100496710c293f GIT binary patch literal 281 zcmXv}ze__g5KdZ3#nMi?XA(N}g@=L&Vs{4@2cgT6Hfdh)y(HxFly>sJaB~%R{}4y% zB=`>q-8z{-`yJeU_k;WH-iO0ulJKTK^+SrkIXQst$##^;E0JVIB%M*&xhLc9`pOH$ z+RWL6VQZpdZpm`#*#*feft9hEoiVMf3L+Y3S#WJMR^hr=7MI1v-MF*q14y?3*bD%k zRKPR_@Ikg&d3Vbz*C>vSXoKTEILU=YGp`j-y4A*#EZqF& qi2#LP?T)S+C)--xgnn9`Kf{yc{<@UXZ!-NN)1T4l%OG3mrN%$Wi&B38 literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/__pycache__/apache.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/__pycache__/apache.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5f118f916c267484d2d4fadbbaae8b84c60a39f9 GIT binary patch literal 46416 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 literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/__pycache__/exc.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/__pycache__/exc.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f222927395efa01b9e069e03bff150be78009827 GIT binary patch literal 17259 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 literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/__pycache__/hash.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/__pycache__/hash.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4f0e84529e55d2cb39aa313c6412f53ebbf7dd77 GIT binary patch literal 2480 zcmZuzJ9FGN5GHlf>5kG_kBMw2nRqZ07f~jYqWE=UK`PkN5GQKGv{y}zjYnQ%dikJ z3x*LE5jsLy&M!*DgEJaRl;A95=>k>XL>SS8q7)}oprgkCrX;4h@Vu{DkN3HL(32mZ z|M2yT-&S&VE1(z}OSzUmfE|`#cIrN{JlKfhWbH9lJOMrxwb$*C2AL^r*#H(M@~<*T86_HctMBWf4cdHuIl<# z;!h5l+Vp+GL*G}swQZe!uE$DsL@Cs!T}btOUw0kU@6_t`QbU|Ccsf;GOWO)Tn3tJU zT}uas`Xa*9*+X@^m09}8Tw8M{zh29;$-zf>Q!ObHdfS*;hah2gwF5N6TM z3Yt7r{YLX{+Y4Jj`yl;@Uib-lP%|^t(+C7UQJ&>=3N=5QOhH4E%6vto$Qe!-Tn!BM zZ9P7?Ce~%ywP+LPPK_{ugP0PvZ-Fd$LFQ8vR_(08bbxzSQ{Wh;5>lF78C$5z>7Z4K zxrj2gUcEnQeT)gte3PtDTa{!}+A%^?_ghA{=}slw;FMx292Qgmqe}FKBTfp&$sGY`eAF@GcM#Jr$TMlT(ytdr$^3jM4MW-?174l0mVVp{S+8Wr(OcqID%K;u4T{Yk&{8x0 z_u4BI>C(&(#5J*9nsb&CelCo|LYuF-oE2L Mb1xnJ&J%F-KPIG?Q2+n{ literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/__pycache__/hosts.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/__pycache__/hosts.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ccbca2f99490dfcaa5d468b8cb9c95132c957447 GIT binary patch literal 1657 zcma)6y>Ht_6o01=>YI|q#7GOrQ5>a!ipGLms0IQjLp`)Z&<5>LfD1v3SBWXVa7Q(k zkpdbzc+8fqg9jIdJN6Hu0SyAD07VOQW8h4g`i>%5yYA5A-Q)ZCy?5{3J@V&9qY5~F z8vi!_vkvgP5Ykadn2Q_%cm)QiU=Sl`5j&@%3i-U1w+m_^<_lKQmQ*R`i&n`ltL2!N ztcqP#t9DJT5g3C}Iw5L32eg2E`GgpiG5NK6ngck)wHk*+ZK!gBshyBh-2V*MYN*Y` zQom+tC6>lD%Ni{|Yo`&|EkphhR7Ie1DW_;SOO5ca-EJ7ouR*L{>!{)BoLlC>_Qd5r z|4X>^NLYBTGY8+TC;HKBpUpgf-*tR?=%eoD)0M|pQ^z-L`VAsYXN-n2qhpi%Y}U(# zm5I)^k#G##Q^!19@fsgZs7-lK3LBQ`Oi{fG8_Kb`lgUljqs{?0GKK6=|FYctmQi|H zQ5OZI@Ln;j^NBtjV8l2HA?*x5Nu-Kx3{%#iX`+odQN!f=fkh1_&K4^vB{_#xgL;gP zbe|d+UOI>avo%D8Rgs9}W6hJ;jVZ#n$ky@o55in+c?luKh_H&u4HBb>$8HHC_KyRN zkjaMVs64KVhe%$5pY;&Hvy8ub`#-=7V8s{M-A4@oA>qyEj)oIzd6adYPt4JzWBQa~ zX}sfpk2*f}JeOgm9XC0hPI`4@Ra}lqm_WEWroNV;wG^FQGzs(W)aN3z&b!b06W6AF zZtGL#_C4krecj^Eod5A(A=#A+Imw;kmrKkav&s%bFZPzVbmNL8BEY_s{G1kN20&_Hp$B(eOp@ zW?0fR!yRc_SQh<{UCo&|>#&)ABuTq6@gE6msg@p&SW#$7iDq>+*{3!2O^dSk=Bxd>7+FGU-$ zwG-SOL^;yjjvytkmCcuTqEcZ?dILz_3KBtqNb|-^Iobi`UeMk`c=uCsnZ5y{?31@K zoj39F{x7b*6|CLGNcX-J@iL0>+D}D1q;olHL!rLNZ$u!I$5FLZR3gYo8*X$1+^+i& b=XrN;(cL>&9xaqd^C#koZVMmb7~ja>9b&;W literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/__pycache__/ifc.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/__pycache__/ifc.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c388dc71e7e2bc16132e36198eb7952454b2ce36 GIT binary patch literal 8306 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 HTBpAOtv7%p literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/__pycache__/pwd.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/__pycache__/pwd.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1b9f1991debc3024787eea02dea7f29943adac73 GIT binary patch literal 28567 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 S?9XaCg}?9*xc`+n;Ql|P5J9B? literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/__pycache__/registry.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/__pycache__/registry.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7e7d9834a176be2c6456223c84a5ba981d2e6bd6 GIT binary patch literal 20697 zcmdUXdvF`qx!*1x1VDfQ2|h&Xxq6b2C{onRHlv62mMoigEZL5k*d&Bql8``v*peLX481gdlNToa+@mS+*?n3J5{H>Q|D$no#|^98f3z5%c(kdGSl=gxK3s= z{-^zY-`NEg1eM&*{j00h!Jg-S=bZ0+@8v&kX=#>l{e#KBn0)SeN&0KbxL1?&@LAC> zNgqh6G%BgG>dVQaGXMHUeLhL^-9)Xpw7`#LN%{!?#Wfm4JAbZeIy4%Rsc#?`o{o%0 z_&Jzso{o-2r(>fr&Nt;+rsJb=eh%dl)2*Ye(`}>4>GshME(_;6r@KbG_&Ji>G~GSg zElZP<+WbR#w8tl1lGNzik{Z)C{}}LmRE24D3t-&xV*vD#3zjsx6=j>(m2K0u-rVjs z8Qp;viu!g|Qd`vc-d+KRMIO(JbqJCJsA+NEy7bGO=qbTdD1QMclG8>ibj-NC&S zuHDJ$E_FBB?BR4Tr~5eV<+P8}6sP++J-}%{k8@BRK;J=rKBNxe`7ozPI6cbgF;0(j z`h@x<+C0VSFsENoPoU%^r>D5&G^bBKF0e7dgGAUdQuGoW9Jpuc%)_$*Y`>@;G1S^eddcroN8aF?Aei zTD^fZ!|a*W=swj%+N*v;z5TX4+NW)skkvbQN~wQWy^G)d_?^S=0ri{e8~E*4 z7u9?CJ*a+dbU=M`bWmLwJ){k&vlBk`>nI=6y4APdmeyh8VfF9eEk{(}H7WJ2;_;cZ zVdSzm26SyQYnb|+a!{#0P84*7>hBbERhdc~Q%Le^PScHF(D=G|c8F(KbdUH0EUtneyAMSz|1t&&`-)&hT~w zFB3&}ayFm6>r@+=DQ#LaFhm@snS!c~74$JcjgpROZMu*xYGVdolGDa+r*pGfDqwFB zv(fbWSB%Z1%_+NU<4U`2QZwt@+wD0lPkot@akrX&UE1{7VPrm#Gzqj!8uh92s9*Jg zzWGN3Y5*%ARD(#H)Fz}MHH0**`mq;byK~H0su!KTctJ1Roh$zD8TwJC3+il6oG%c?fWbf(gC(SD5mL%0TFMrSXp|AcNz9pN%+DP-*zaQhxA64Ie&o}SW@&B4sHDdNXKbvvb3^nT@HFWI2*&G37Npf^ z;=R-FoL)XsitZ>!cT^-fa$a6bDoXVzRcIh*`LCSb`Q=(mCocJ~#K9K&# zII-QV?yV%Pc%JDE;+n^3^S*k{4@p4+4usq`k~9x|`l+uTTSTjre?;#h+%L-cvu3QZi4~prI6=oE;t;)3q7Gx9yL^H&Oou z^fAInFulZpl^AG*s)`o>o}n^55PJ{symYg=_4NdyK6g(BVtLUHqlyb71l;HPJ~AQW zhJ8#Hk43NbeasejG@#ZxLUs+PG?EJ0er&YuSF^eu85_&wKw8Jf?55cnHEn9NifT4v zqNI5&SIDHzY$0#hO-4ZnwNLqY)$JgmqhZk86=(HB)Q=W_+v66Wt52be@f?x`324;T z@&38x!1v>&j^1)dZ>hDf+}gJoTn)Fp7kekRbp5-pmBPEr;oVkvw^R21Xeq3e!-^GF z))Jjd+PCh#d(R4Q5(@T4UhH1lz-NY;5eSv+0~uLx2`CD8}(|NEi@j3qa!I zq3@-qLzL{Hgb-F7o+Ld{LK3-=MY2%+SMddd;fh2_OlsY}aCtS^wQ#i(2n8?870Ht( z!@=VfshV`i!9h?sH)(1LZmUR*iGYIMZMUR>;?wFeAy5=I*A9k|uLi1hc2ioPEaVRt zH#M{o6?SLZq}|V^^J6nNZmAQ8$Be1;&<5>At=)8kg{=)5hzh$!{L(Ooj~%zeH?otW zM6ZN*+&Y69O)2)6H+|t?#)6yiJ zwzblsYMGJF;jHmq*lkWbrwp5Gw>m{NcoS|()<}a}tLT->n#H<#1}2QDnPO+da#69J zoYZG;c$yy@It=kZy9-1+y1|~PS+IT6aGM;n!_<~=$8L6>XpZhPjTW9VMK3#^Q`0kJ zGr4p&Z)$hV;clgkuLmQkauB$b{hELC-^^wCzHfl^& zc8FUX9kQFm(YC_{J)OyELx(mXAEK(-WXO)zz#p<(Y8jfJC(BctnIa6@U{q0IM{2m% zi2e|87pSmtQx{LDX$@;yjM==0V{C7HiMQTvVYOLxNr}i#q;t6&koSu{uYWmx zu=pA!hYmh9_SymG>9su&ESd^=QWNc_nL;j`nFDPt@EN!L=x&EpZ6ZCJGwndWkk_Du z%$kKdQ7{1h#-iW}h+B}wB_rt-yi|E!+DP-7msULD8OCs~dh5l!bc=;4Xmg&{(B?KA z(Js?Y>ooj+oNDvYRfMGqIH8ewcnM@w6C=Rn)4sB4`uVg0_ZF6CJ3GE-t4b zlzhQda)mp1OD1h-^&PnZZ9mAop{Rvf@FfKbJuE;OsQPKB<*r!^uqfth*2e)s#xgkNur`u}17bFf zLkB|O4hU*;JDYY5iEgQ2qwYZ%?`P@-qqRAu!C=}j!i_m$EG26sX~d$vFpu8J=5mUv z<}<4cDrBcktC~V~ zbsEKhtvoxC#oXwWJIgQ(eTDPGf|?X2>fXlHWV{}(+I$&GSQ5>uLD)NV)ou(5XiODm zb2a*xt~Wo{y@zH4z=nO zJg_O0)@#oZXx5=z6|5pbu`7A%C8)?pldyQb#|V|Jy+#~IfO zg;PPW_JB%Zs~!-X){Ho)G1*JcZDw3M(SXQ zgQfahJwCFJfq`)car78w+oc zp?>(C;ib&CPQQD4`HK&u+pXyKwd7W7+nG}GY&m(>iV-7z@2Pj5TDrEp=Lh@$WdF*w zKOX%4U@5x49NmBa#fQ;BD>}$M&Xtnq%gOUr>^xxT-ujJa7R^$iV=WN+=9RauSn;8c zk3Q%q1x}R%r!4no_I@$LLOo_f7n#+Wzf0iyA4tECzZ-4!c3cV`sOL`Lu#!>FgV*s- zYEcU`xaT(`3)Jw|@abD&?DsorNS7I{wT4>Y`<_n?&ij!zeb~V3y&QMN<2O7b0`md=<$0(Zvy#5uY^}9i@jzKCRYUVoS?X5+Ziw(TaBY2CU4M`D+0#FQ{(<%V z^PMI+B%Dia=J`eE15o4Eg=NrMdS^NBXSw1J!yS7Z+^LrI(?ACIq;1Tw9L^~mp&I;K zDhTtmre>ipfsMPJ$vf%l8>|oz{U`gCo=@j^WH`vGv|I=WMuEtKNv=Nh@&*P*O}6=4_MQgzzxqpqk%WJpl6`^@Q+e}M^?{5ngBJ@ z?!q}ueA1m>pQEK4G}Og(&d>yqb zK(fI2c$KlS@`47u{7_D2!M(xB9i?;_fdRwgHSGr;UNI%7z8WY{0|)P~myR4iwpzej zShIp}In7!zwj(W6V|VK7hOfQ&502j}1O=rJz!v}jrU3tV7GCa}>(&sXH&WIwgJ`p#9W`^swf)@AKezudAH?`heK_c#e0 zi|t#M`c}6p%jcKRuk2mf3y`*^08;yYG;QBcP4`#A(x&a-xU#5z{rRe#k-C5M<4vW& zQ{})@mV5smGr|ktFJz}&!IC##4Tgek^RhQ;0E{_VZ#vl6qBjqhctkSEc;Q_05}iJA zU6Xc8UTftZN%zart5QC2N4o2ORk|bBi6v5<^B7`MEk?y?eg?5d^;T12%PXEYyKjX; zAHe+T`fn_l)Q?ATFjY(JNwdu}9$P`xCK7}`44CBWbS^dY)9`x1V%~S}Z?`5CSm9hN z9zo14gY$fyefOS!-miaGg_r|lPS3o5#bX4i^-?4C=8~^qh3*CJ1@i$CQ|<-t1x)fZ zI~T;W;H0cJ|1?@Zzk5yI-ToWG7n%?0JI!tG*qFgO(~Z%J{f!ZhsV(y%7QxW?qiP52 zHEhQw5gO;3Z<-I*@A18mxznwsH_S^vjn}t;po>xd0H!A%kLjt_bT1Z7*GN>V5@*oB z8e>`~%8A&%4J5f7Ge0Q=K zcJe26ia06m!&aB<=;5eV4kM2U^PG;;02b^TB2<^K?g-JC@_2|z8iWEH-K00e_z%kO^oJIE)ot|j%uky7+PIeNefT&4Tq)l%SUnV(mixp~JgoA*9!-dk$!EjRZroMWTNTQG{O(WtBB;aD;6U%P(|FNhyrJ91{V zy9X|h$QHOfB3t0{h`_c&Nt4vt4ltVhk+!w=t;^THd)kWa`rTS$69A30)sod%>*9F~ z7HM;#^*UD;&ciEHk-CDLR-^HyJ&Rvh{KD#v-9IS)N%8*fKYsK3ZQM z9lKYqFTJvO>9@^NqGx&2{Y&?sdT_>ypAur!N_xD+$Zrp3}uEnWOu77gf+H|!lraq!(#%`d=U&PMH z($D1PGjadVT4bbubt-l)D*Zg#e6HL7^G!0+43MaL1@J`3{n>Ys)SHF8@>o6RM#!FI zY=$d^&TYfx<i9vs0kes&(oRY_*K`L!(^7}>D8RDcL-?BPe%ef)USy!^8CfhvfhB|>j_F?i*Le0~gq?)ZVBKdf`X^}g4MGqF4`7sCQcK(WSFG4JEBegJ8!M-+ z-B&&~|FUR3am5Ne!yA9S?o){B&cBTQjXST0y_hgBog^!8`+7XkV_v+VR&+4k|0!Sk zM}TmaLxQ{pRQ@uAgm7U}(dQgQ85n>Kij5#%4;tGBfj)4wRBes*URVVe#G$}~Y85~F|*1d|Wt|ZI|QWGp4=`(A>sHfw@AVi}GLl{PIT{YyE=HVI!>xZoG z?Z_D9e#RjPW*UB@G-T|;j7wP`i+>~xi}kC=L?pF^5+ty)W$C!Y`wP1I~OoF;66+1KFvX#g~y|^ek_dR@t52rDEO)$aeZXNTXIS+p5jfb zB5f@o3pVjW4lYFO`|LC>ZBrJuymzzqV9pw-{t_cRu$*YOZ#g?^5`_e_ozmWsw z$N_vugYvb6veH#b^pz8RR=Dq1@wRe&_sYdT8+m|WAScjIP$X0tiC5)j4d zP^&uM9Kp@|rvjGWzkm1EnGbFewUYGHUSruj=uExg!SKY80 z!*oXKjC0j?7e=%I8PVwTx~dtiy7y!pQUmpG*LT6c2Yv~UAgNG%-IjIu^k58;Ha3kl zexCueiO1vR%$w&$pO4X4b=X+NOpdL=D%Fg6z`&;Tk5I(8bRCqCIFnH|dr&Z^hOi2v zthdwI9L0?@TNWtgk|rTqin5v;r`eK&! zYttr9;hka?VfuxGkm}e_<|2}?ky?nohIFMeyXh$(K zH#?GQW(sEe&y2iEpDY?=Nznfz4j7X{yZt%wk>pu{(Cl`>hbJOu=>G`~?aaU29{t7vEuEe{RCQ2>a$}QU#Ewsee?{vM(9hZBppm+qB9d&?oXI|9K2kD>``-@SBc<@leTwmJ^u z<`F^Cf0@|%FtKx`x0KjlPVBeB`-O_#`!LjFg?d&K?ThAW1ojYr@CnW^ec!t>aeuNJ z>S2(5{?9wk1f-t@5@$B~f7T@<&7iP)5$j~K{=e`5vR3-mYWBzh5r~ z8<31Esvm!$!GEvLi-ACi$Amc66L|`zK+7sY_5im|BS*{7he2-`*y2O93Qwuk}1+<52iI=?A?(s*dnaQdPBjfIdytdoePO1e@dd@PwbgHg@jCS6{fU z|4USPg$(^)p`bWKhu4`4zFKmb|CqB98)(=euqESF^c5O5FgST0U&&Frd~~gI--=o4 z>@Ror|FZMRhn-LUWp}CbY`OC+&H)-Mksq_#{Bh4hh9t3Jr1+X=_4`tat2+?q1E(+` zfAzD9Mlk{;LpxJpegxAhEZ~klON7%2i{`+@C{~zc;5mLsF@C!oL3@D) zdFK&}Z&*d+$6&dDblI}+(~v&`uQ3w(lDiKs+F0Adv%H&iM}tH$raJUFX+6~`bUj}1 zSLsV@^!teV@`aP^Pj}!n@EP>jGw>HHfFM=0Cbd!=TsLsXgH_{Iq!LFNyl$g|kMW30 zR*{ZnfOPIodX6+rc6ZgW;f-Z4SLkxMf4%G_hQZ%iKu|^_|E4Y^;{l}`Cina`#Osg< z;($OT={js{zrZqq9Q+sjFay+AsX%6y>HWB%gLrlo@tt+k{~C?Sly8s&1DMtYOtYew z|55b*#eaVJlcSb$xuje!E0?XnWpQNp(CEI4u5o=KF?7*jH;&=^YCo(#*Gkjp? zl*}SL+Ynk9iP~ZnDEv+Tuhg!iuH8VUFbxBN@T|1gSE(94deO;aY~%A|7fc7K9f1X<2$vHANS3HIeZaw?ogxT? zNjIAjKcR}BQ$k9h{+}s%NC|DC*iNDayiM!AcK$qkLPRs4(mz8J;uppfSRGJdSzeV+ zSkj3~vm`e!MCjkD6kp(fR#)%RWVx&Nez?>%RPGvD@~=ugR{i}*+GN$=tI|`}M)#_; z$Ev?8KELb(ZTNpk!jgQ*k_Nr^s&v$ZF3-rI&MkPA_B#eb4Q()$2fOfitVBH7exM|EOi~T zI*(Q^iTq{M58`i2RIZ5pGqTin$Z8#|Tow6eWvOGpYCl+cPUJ^qsdLck7^pli@-N8J zrsG!EvC0=k9$41>gth5-<(kM}m!-{5Sv^lyUK072-TW&e|0NfmS4Do*9q-E`{}s3X zHIaW^mf8^`wk=I0i0`Tz$O^W;!;Nh*!iu_GC ze@o_lFhUhXenysBdac;L%2!2RciS5x4LtnR~==SBVn zpVYF~itVXGU0*Xv7W}Kt?F;w=LYaL1_~SH;<#D)l(2 z@_K7)_V<1N|GNN6c9KceK2Z@1P%(`zlZn|D^+;F0NZ=^NaHrB>onxKRuCXqDUJ>bz_KfvJ zd&hdCePeym{;~e(p0PdAy<>Z$`^NS~2gU|CoiDOKdSL88^x)XR=-}8OKdZ!?92z?m zJv??edSvWK^yt{p=&`Y5(Pze`_ALMQ zN6tit$A+V4$IeF2jh*9X)sgej3u70Yj%i1*=BLiFkx=E;=k(qOYp?NFwHLjs!+1YG z_B?WJ3O#?_sWxNgpTGKok#n4 zL4MbYcWt4)IqeDYyLPwz79*&1&v)PI{H9VUbpPHYYOyGCBGwXW( zl|3Wv?rh1t8kq~I@ldvuKgPcok7NtKHy=xcvc>bW{H47>EkzY7!8Lp@rtc;6-I;aY z2&hz?FBqB%%tsRAR|1pQLbE|UDxM6_&4ko=^2lUhwl$%MMk%e6>cU(iHmwHcW)@nN zIiwFvhdLGReQ1{7qKQf{91mQHgs3D}=;kDvT6QspqED%6OwCqb2t=l0YBUtIJggd? zy&i~!t&b`$#jQ!&OR{B&*!cWxcrq3YWlQk=%0eO(M`L5N@kHo*S?_cxp$2Ay;;~De zoz9kzhQ2qCmW3{aZ-!^Hr8i@$%vB~6#}g{X-+SWt=&6Cd<3neTzc`vLy|SktnW!52 z8Vu3b_Oh&Vp2k(wUp*U0gs+Fjkv}{;J)T&YLsQDH1mct7@HpCr=h;%4IPs=*c0MY< z7KPEs=}^{3?Hmt9LeX(_eI9jP5@ioRm#v7;&&{c!czk>^Fdq-K7i7z47Otq_;CM8Y zn280mRnbs17EXqO<8x|kE~F+F&~!17P-RYTKZOq3c5Dy{Nis}i4_2}MK7 zjc{T{(bsdgnLSzjT72=G&529XtuY*rCj`E@tvRcXvI(%$jOnfcjkSMWa`!|k@?Z@iAG?T;M=g`4@7aef`}}1(G&;8LWy@O@eoh|mQql;vY>Qy;AcmN zSSi>`Q}YpCOVPkJz$$=iz~Z>lqFf6t#FbDW9>y|76>2EJ*=OdXSQ8?*ZI{uu+k^jK z_2c$y1?L=2?6pP5>s7a$cQ8PU&N~H3ys;w8%Z~S)PRCt;5IgSY&XJ!xutp2Id$Z1% zdKgKwg$(-S`n-KxJ%)$hlpdZA&4zBysYjDLty`!2a3qFJ6F=H*B#0w@ya%_h|I+c7 zSa<2_A^hEUE}dH5wS0Uz`0J;a+wMIlo?7qfapV!3$endz&+r1yy06ByzcYAu@b<}d&z9xA z8IPiQ6fAU4#jhTC`+&7Y`2M?>9~^&>cz7|>bwcYpfd`rDAq{uWkmeamyN9^Zwna`e z!i#(vOB-_mQu-P%Q?<*w*cTlM%i^_@fbqJ7&f1``tHvf+bOZ~6IZOCQfTN?11W^?7 zLMqoMZhMKh90_KlM3NPP*m+m5aM5AUwPM*mX7XT>eIwqYJ{F5U#*6U1ME18OL3m1D zSB(}fI#w)g(ce41WhYBJF|rZ1fp3-vT!~z)pa22nhD*^7J(=U^$m#6J^kP80KS-w-IxWW0EA~chbj_f z4=U~D*)mX`AgKbNn^a;_)C%0PRi|#EApkKKLF+Inr&iPR((%biC@?#&hOk-YuVjlP z>BRL8>;(zcHz4|!Ed*5)Qq}F0t?V)pJzUPu3smA?vPGf|aaya2BDPrhxX3HWHZ_D- z3-}-Z47b~k$7PO^ily2g{owWw)_wKMjUUWse9fA#nb6FUb$_kqZ@urj|I&j{#(!Az zA5MD@ulf9`vcD;EcxsnMR|?Z*Et#?wt*ixTr>y!{yWZ|vE?6GQlqp)7@~EtJwXF4i zai*+SE9<>Iv{qa$YFchtIkUQ@GrgsA-B+vmcCNU7yY#1}Kkh-r>;J{{_uLb{@F~$8Li?>y5tP+>*SWUW8wBU zUJZ05d%F(4EISYgmTR)qh> zrNq!z1Pj5klmv^w(v=2_aW4ybf+cU|?13?FuoUmgLlr?^(DRnlj;tVj@ZxP{upIX) zW`8QxZZsh9zm_{3yt*&WgyYI&1RN0%R}93f5}2Ed5X%FepzB6B7y_1>R6~ill9&N@ z18bpN2`Ru&LCy~hgq1@a22c|woxd?d#dr#9gUbcSAvVBpqBX8WWAQ`;`(;)!Hdio| z2!tb@*4pE!WggV+f^u~}o=|2(W|c(PC=(O1WfKz$sxf|!&jk`Qc!s7865%~KLG1x~ ziVZW-UB=Ds?nbvlz)G>fLGc81mNKY@rUoa7TI!;~K>99yOT{MyGR6?0o77g&f3p}9 z4Bw5IdTlBayCKUyu1?QK!8gXarX%{rzXo})>q9!o~;;mHJ%ke0#M6Hz}f+{z8` zqWV~)Wj%7R@zp4WXD}%ESsMOsMdA64&&1{P{6dI>$6HA7z8UEBd8dQ1VM-xYb2&G0;81BP4EwBrC47VsY_59)t29$a) z8yOAEDT%;zTSU_qnENcb8p6D$~1gE7WDPsZj6 z5gP4iYv^m}R2uqm>+bGu$IhKqhDV=Mf}m%Wwub)RzJadZKKyYi{VDpEkx$Cr0Y-8Fy`iWbiH9>!h}<8ysH7U6S6}zp|l(TD<$W^ z&>|8GFgY@%#-cRHp_|A6>4I`S9AJKf0XxIXj*fP-d@FQR21$rDI6=fc@hanDt21~- zcA9Mio^Y4q^HWpd$uL$8_EZ9Ljs;P$9rSUfCj2`dVInILiY#;j$3Uck)l9fXkWsX$ zb!JKQ0R00b#z-53Yyq8iycIw~vFAH6Iy6Bc$Sts^0dHxl#C#a#NOA^nEIz4*uZYz{ z%oz_BF=(7rc1IV((R3L_j7r)Mo0=g7ttBi$OCc6eK&Vs+5PBud!a_ZxAVQxKy;8e% zfZVMNPbslzIDz#CK^`)q27(|Gz(E#?7e*;M$cO~gY3wpeOv@ylN!3^6C|IY@ID zqY*6XqIb#^z$yx-Ss-Mf&x8%E69K?m77$Ve{WKSw4RW(1vFU!W3v!X6;a9|xd;Y|^ zlc)QQfdVf|8E8J_1Yq}(kQWnfrA;nkmMu$MV!^g}yJaa!%!Z}LTXG&VN4z)n3w(o7 zx=_r9HApxTtbWQGXr4920(IyO`g9-(AW|WL(>3|Cf zY5!RIty2={M|?hz|eY!z7&}04f75hhbI*nLQCy`~(*v_!TU3gEo^3%ff@u0+SGd z3NcrVx$e0v3D=oW)B%-PGQ@ez#+aU(;~^!eLyd%FPHY(>iFqPXLs*bT$Dbc2f()vH zAkAVp20}ow-hb%@bN3}N{v(qX`H;hnCocT| z>fZ^8lk10jY?3C|lO;{6&njtJblGJow_J-ZNUylAc)?i*WKFI`$2ir+*P{D}dlsEP z^jW1(!8}<@!ceC}hznjgN5-_FEXvO5lAM2IS=6G_-r}21N;^)1X3;{YI*-=*b(2jV-d@T$KslHC(Eqb z%hfStRu$Z`E+<65|``CFPH-3)?;G ziUw|GJr-!$UI_h8C^D5Tmb0VYq!y6gCF_-sc#7h*mxxW1qO=ly7?S?{p(&zq@Z$!d znjZ?8w5O3IYMp=Db|~XJtoaT@q*mUyzO!NFaAs$(wzGH1yH>q(rEj&mDXsslDV?d4 zns@u-3Q@qCuP*ItSg+feRxV}gE^Bp{)0LOk>$WT(zF(86>(=VJAJy$yt=p5S+o#p- zOFjFz$kEvPsA1n~!@dX8nTDfU!_jof(NytL)4eW8`n=VT9VMl^*L+*jzLw7`Yu@v{ z>s#?;Dm%5x&NTn6SJjBQx<8H2OwE9Xd)0teH2`H&`R?@{yH;9%H=f?HC$nRZwqwtd zXUX#gJ^S^@`;jHj+RhGY{IfG?ym3$lc1aR{trx*d!zTi!$=x~iJ8X!I=*^J4+LXel3S7*WJ zQwIGaA=|5fI)mW&Nclz#-5`tNUpLZSY%lo6`D)hr<~Po=Z=8cRph(liK(Pixrvt?m zy*IW``_MoWJ;$KaGUR4*ds<4TzWk)c7R;IQ_z+S)Vaes<(boWMZxsMz7p#1Tb}YKS zORA{?3F^PcILJNnjk8hxA^P?uRY5i+aY&OBB=?AC3yIU7!y^`Wk+1-d=rz(6#le(J zLj6ri)hIIKkzl!L-_jHc)j}CG+$tbt7=|vL7KmsA%_hn)Qy)i*;&)M&z!_^w%Oj<4 zRq4wpdo*RwV~4x;DBqUdYdahN+vIN~e|_=&Mf}*h6Uy-Vqxapv=ldPsLszElu+|2+ zibrc5-M`oII~@;3AMVO@9M(Dx|N6P*p_RI|hUVXH|7rVAc7M40{`KEm{GG+0fB%#3 zryGu>x5>Y+AD8NtF_b!;Y?z-Vc^rgVq#l<2)&)gEATg5MZ7z9T&L@Rcoh1K`K+H#PzkLb~wed?dgXd7LR2u`i)pYai&o(zKyOu9Kb~$S6AJyz$t=WBl z=s|6!X1`Xm-%Pd9H$fJZS?GbH2rpCh$EXOZ5t5^65N%pdZJj&`Eo}jPP zIrR>`ErVgqr!d+hL)KkNLWe*61dR!(Q? zcWd>#mx`8()~afk>y~yb?N}MQKd^FP<-+5l!YZtF02eu za%s0f=~ie*5Se~|bseK-Hx&U&BjQqC_EoGpXZPozKU$g_r>(S_h~%T(S-%yP2LZh| z(k6eqZauf;xZjE@VVbgWx#+ikEfyrm^h;j$9>;5}Xn7Gf0;0S;D*$j8IW-q+`t{U`X5 z-1da6$v|z4w&%Y^vfB>YmXm+FwL9bK(L6o00kAE-m9#BfAB<+ocWLFjK)m89)Y(gu z@3h@(OAV!lzVL5dYX8*rsVlwhRK|Z=^Pf(8Pp|vxXge>DW_-IeAF?@1FCg2trH)T0 zKb=f(KbP^J*Zk+x-t$D-H-A){@wIBcR!aAbbG>HU@}=cV533(mr#p_OYmQ}Vj%hW= zQm58@w8yvsR1(#>-K-P(^rcg&WV)nY>?zACa?q~fJw<~?mO@wYDXyS%-iaA6NcKbg z4w(m}hzSzh5#oVjBVdYFJD9zM$EP-5@#WxzGYzW{|X%1XoieS~k-qcg@B&m?P*}VzX1>X-JEM$r|0vMAnBE zz_`Svl3iuCM6Wtp_uPXa$j-0VeVZuZh|_vs_bVT5UfIn1rQ=*?*LiK% z`HcU9=D(2kULbSD5$D%me;p_J&TkYqIDXo3-0M1R6Ob8phkuU7*-cSz+m}ne@XFxc z95AaQun11gmBa!J7JgFnGraxSNO7ysigrx1EgD|^x@w@J6(gVijy@L);hRuwHP^E* zE?Zeedd?>Z#|ne+b9qWx7V|7Sn_{<;yj8U5R%;fEHWs1TbIw*HU76l4>!&^4t>VSv z6^mfdOqpxYg>;5E%uJQbpVATq@G1$kV1SLyGpt*8Sqy)r)rc+>{$21r@*Nk0xwv77`$LL%d`)}O1o;?@|l&X2YWxQ`ZV}Q_34X{Q@@m{d`YW( zDOCt2)4yG-YW(P&)-?F2>F8?H(TDS&j(&RO)6rifGfii;rnBo!JzCSgM@Xe*q7x)4zUfy!iE1S{t;!@2>r@V3b_S4KUQEgYLO_Qpb-v|ZbfZSA z)HsjId9r#9Z#I+;FS_k?35aUf&wzSA#(!~v8nViYp-NnITUBehsyExaLVM|8WNnld zFnwX57lUB{(KEuchn36#dUD+xm?6$^k1QzIR@KNRZAP$e?ZRXo@mUZffF*SCfg6HC zw_wDL#nB{4()nTngR(_{O-9)*25l~kI1Ev@bo9_SnlKKK{wKoN6QG{pHd&~lRk67M zEFuMpv?3*#eqV)oxY1iG5Q8H$*kta*Ko)oq5FaoFW_KtL2~aw@pa9&)q2y+05d>Xe z{;-wU#^yO>bVjywRvQnP7=TlO@qtM#Q4{P`kpXkr7&ijvD}uz*um(15Y@TncaKZ|G zmBUYf$ww!~fmhib(lS{WL2Hp22BTC1v>K#aZsV9_+n7)?n#8n*Zi`6t3h*`&TA(o) z^=(9-5iutyGz_v%{6RJhgkj<|O7s+(5f+|T$l{P_7e7GT9@KQ+-d7KP`%2R0qYU^Lo}fnROdV6=q-=q0^(%znR-_{R?R%$-nk@59v zzFu~Y19Zn{gP-6) zt^LTu#HSZCzO$O|Y|8xwJnaI@dsp`SZu^6iKkxaZ=MP@_H1RLSe=(l+UC8(@Xub>S zk_-8m!{A$g%9z87HJ&B_1+=&DmihSQyDOF8cIhUSQm|0E-jvwzj2q%n3lA48S}fQ| zJWB3C^%#C}-Gaz5SYr8I1Z}6=il4~U$9!+c1W&|NIT$0Sys{^jSM-S|2X7TG7NM60 z*1N_yH||-4dz9(KB%Z1TK`+Lqc+the((=G>K~iM^h8&!v^D8?12Tmf(U5S(kK|!!- zZq*?W_^>Lr3UH*H2vEi3ey}Gj;7vEfWp$e!r3n?B5@VDgkQu8{0rFvrwE%>GGYVTxDCl1LhgXQ)KW~Yropq<*$fpi@mhG{9mCpj9Zoy!0gW677LQjwD!LyW>L zWdxKfF>r}6BA$vskQSZ=!UgKE_)AcJU6LuG0bv%l{RGb?$!0P{6{deN@^&zxFJ&hP z41PHODw8$TZkoy&K4+T;YZY7pJ{Qn07Qg`S|Zf!-w(UDd|rca~_ zMkb&xs1LD6M@}fIAl zf+6`uUBb^0pRhpI1=*oM1y?99Iid-qKte>u7T{DCC?$EZq2bwLS#h@9wm68TVxEam zfly8|@*rHY0pkMu9iG2j#9?{-ZwPVx6|#e20pi%Q-11SADW5;Ys;`3?|Dfg{OnV2{ z{F~qFc(-GDbfqcdZ-tv11fQF?r^?qn{-wa%hf{~ww(QWhbd!_y7o-6AZ2Gh5^!ArB z{+BiX%W3b+YZWz1qj#?ns)1JF?wf>r>QmQH%kt@!J?}$P(5Cs@;B5Wgv3HNH?8#KO zYSpdlRa=$^RvO89d|X=SEBmDbx4))a%Jtam@K!GE)vB9TJxys(6TI&d zTF5j8+Y6zTG;Wuj<%L@wJB(Y4 zqp0F>K|x_BInsCj(uJRIy(}J-cnV2+ZMio%iu{iY8VjM{)o+x;|JG~b=~?Ga&SJTr zaJCgTJa**Y#5i=L)5(IPFW~=h+@ci3V{uLDU+7b38NYJl8yQCBdnY3NPoeNm43(HD zZmbOciXQy;;(vLt5J8WM;B)AP$DtekhQ$bWRDwW9HSE#Y2T#RZ|HRr*4Ge&C42j0- zC_Q{2``JLq`&Q}LrqJf#o?t~@{(3z3<>lXkrKxM%rEY>_n=Dyi{Fy+L-sW8&8|^p7!8rbKcWl zW3F5B=Bf{~(wdi}ALX>=J>3)BA8bcE_Xa!A`+Z2WJ1@-u`qGIs`|-9b@9hCR?aq68 z5N+tmdpd}cdQsA$lmqMhFhmCj)E=yfz(J6%Or}UW4Jce^#R%&W?gyd?PqDn2i8hKk zqkEe?%RxW_y6TQ71IiH(v6+spbd$_1JD(sV1atC&0^^!vQ&a5NC(Y!k1sn;&Xh&=o znJ6{{IU|I@Tms(;K3`|X`H`R+!E};S70Y75!t1gS1mKyxCgTN>DR2sCSMFe2jJ(!#`i};v zqeBq7Amzxw9_=)J=}CSYLy!|9Mj|j~mY%k{NMR69NM223m`U4S1TiV~nKTI?jnVOL z<-~$c;lUWt5SoC3z#Nj$#2`>XNJ77`r=Loud2fT<+S*E*6!v!_1{enav(s_CK}1iT zx(W9Y^pRBL5L)P6MW%@fc%&S;sLqE#66-cyMrY+{8hsL@V(*q-p_oePFrn9ANQ=6a z3t}Ke6U-vub11q7_d=yjd?l)tM?mzp-K2Ms8led|DLw-rgGBfn9u;_ia*rAy0OKTv zAk3nwJ}La$m_4!9t#_4M%w#)==o@A`;qEpuA;t-7o8B>@tvkHwVhs|8&;;ooBSL=C zJ-UFk9DNaMD;Bjn!dfTBHWlcIK88=6712~$YY}*|Ma;IIr=PUQpd5-#hRH8Rx_Xjs zigzkIXUax1AW<&8IBfEDC&RFc3@ivq8=*gWDL!?_0pT(z65AoS<|DM13M~qpLF^bIzH@dF{C1~ z90=wfW2{~z`jd7YyIRTiTNdOca)Y$H9I@Q*?=a9b<%pq1+}?aRJ_A^Q_J&L-B3waD zzewG22qHZ+kr$CUl7=%PdER3%;HnM?V)faEjR*SF3Z7CWCvFBzwCHk{(toiT!6317 zoH-vOhD#&!<-#}Hva01SMHB+-w%3DYL5mASqHC_?#P#>G6dqddJ}U$TkGz3LB*E6_ zfPWESjW&@G8L(;?;60eJQ+eZ8;C_D~P-K#eE5gh$>Wsw8vm zt^_Q+O{G23U<3(8L#$7k`CY{FETyXIdsS0yV+L6jI9$a+ER7KEL;_1i68>Dg>Ww~XlV(2(?Q@M>)#cHEX_U#Qj z7K=Z0{u3w4A{_-^i-qu*@#(){Gge}kX8*`JdvLMvhx@I8vK9-i=EU|E#J5^?s#gs= zD|;UJ<)Alaj8}LC71~-=^uu9mjTqmepP`W-8x2j6`bJ*%7N`~Zse@(GMYQa!1JQ+% z&n)RjPHQ+W)Pp(u*qAr8%Fs*SDFtg*9vH;pG2qI{1z>nd(#UWW&}mZmPQv;+A-Id! zm-UCrX zNJy^J?P#hnt^&&;1?5m8>tBh)-=Ie69iuyN#bKMBnCaAYk20u(G0D^(a1X}pGH$U< zgx(Yac!J=jY1yFRhBZ~*WazH!G}3^-L6`?IzbEPxKI|s@NvjP*U^1LE2)5>;3^^Va zcXm=7pnbVRvZ2-m8$hgwyd7X!&dj6>fKAE`v_cO!$#X5(5?%S3&eZzzpxp7i9<-C5g6{`pfrFBqwvvm^pzP)ycs3@)1@VNEN{{5q` zy!_I0FP?n<*~`zpaPjnuFZQ1~ed)xWp}qTFetCF*YhJcEWqWqt$>&~pe)!DriAxvy zpL_1ind2v4$WNM}q<#6%uTz6=<&@k?IEXS(|8w)SiY%tl3+Pw~m53xnPzXyHIU*X&<&t*3W&x*C}xX=0R@GyQwhN2?&ONmDq1Yk0$ZSEz$4ac zgZG66E9g$FH!0vG>Qs9C3M6M=bJ(X6DgsRC5qTJ#>7h8RQK!P)!s>FRx% z>U~=EJ_J^--;p}GRCDJn^ovU^OWjM|%X{gb8d@jEi4}LIx>>7kPJ5eKJ2a9l62uFI zDwxg|3C1E@B#4*fK&!y%_V5v{gnG!hN^dVL$f*%=%?JPzU(}E$N z?V1fu19rp83S3GLVkZU?Vt^~MZrV3$mWKR)quZa;ZJlnU)KdRWnHKnao=4S^bO15B0i_&|Bh~dMz=NG;zTUdLgeciF-?UmQB0PZGxRRa#!Dbj zX*TAZ4u9R!)N*`zYUN6%s$HvUhoPw3Q`!OzU;b?iTqx6??Q7dwmd~!-d{CF!c0k*9 zAa(lgh0i?|sbg#Pjqt-fb$?H$zDKL?!Sl19d#X0h@E8HOpIDafv7@-OaeZ5JdRrI% z?z_b&RRq;Btdz>foE+idpU{sch zr+rQMTY}?S(=y%YucV1PtiFtdlf(w=7qN)U78ih}B(PlIu$p&S0dp>Jy^5N}6|4k6 z_RttP9gAg%eP3pc&SUjlT`ZgcvXIlS!|SVi^8*5zESzs zxI2l>vibM`n~5*nCC7Jvu-N*PdVi||mij6ZE#|MQ zHTw5p$tO7i0<0eQZK}^0^IPsjm!2LicP9uu%ga8t@tszYM6fK;r>CQQ_V#TwhlmK} z5q(DNaCguv=ik=PV7a{<%#a(SY_xWc~$1-RcLWTd;$V- zH>}LlHElE2N|oW`=Nj4Jgej%Ny1I z1tXr^VoeSy3yZef20Hut+L^RK=|za?3))YB_MHexG7kcT$z*jAMc}9!5zkHSz%M3B zvTjlIaiu@0BGYPQ6^z+Z@_Di}kDfU+SZ2D$-Cz%s>IgiJdnm%mH_;zc)-LQ^>bMe8H`)8eK5y@~wx8`{=7_Wjc3Y#4yE(x&}zR@y|-N2>OJ zv8nMR*nVd=b!(ft(b_YqGiz1w&o3_RXA8j>GyYM{KbrQAu9a6Wy}4T6kS=fdd{+|% zw%U>Q?pQ0YgdJIF{}-E@Rz~l)WH$9_oBF66)Qqz#5_fOjIdb>N@?LFg*Q%#0?dd{S zs?Rz5pdLnz?IK7%jrfbgzWw&hKRW-%=QG=$&-h=^{4b=vFRXd0 zmX5zYm>OKJTWR=>t;<{4hTdWTS+@<-;h`F;s7E^yG~tnV`>J>Qawy|%(!5P+ZxgdK zch7P&DA6uXv}4t~WBGc<+oE|}(%zP}EeZzU948_InaUAKR&G9YV$3q$LCwoXmm(#f zTvO=4czU)AiVKU)F^OeoTtw5*@2{3{v$Mf}$Fn#8GTC@-U)2m2njJm|@6Kc4ZQ(EKOT-V+G;>tO2$f@9DS|G2(rZhv@u(fh_z!CSgK6(U3WP8y65!=O%$RMs zM5S%U-)qpe$U-(6HTD0-0JJ+9HCe~(Khc=kmZB&mU>`KbBZrnImoG0xm!dfkiw5lw z4cZ}knLlVWMZC}|@x>l%1bfz*G{u}kaLS|(I8wtp%2*trMBGdq{02(0c7v?bC07YL zVU8M4;SK~>S#x(GU9wrUivXH_Jq?yUB{CA!hX!Y7J6Fh-2M$AMg-v}(Jdj(jJP12V z{5DF$cW@`-Vp_IfM8x4N7EtB|yt0UL{$Kn=uuU7u1k-(V9^?}pjE4}SWbV&!$i{%% zhw26m)mbD2$N_mkAg9(e&No2L@7h34O+7GFDey}FK)PgK%B_`bT@GqHy3_oZzW`|R z>Hk-6<7Qs~6w2@RK`>3UNcfWBXN-Y?zBk{~wqS+I%ezpHF~D|)MTWFBax9WS#FhLL zV`-U7&(M=W-YIP5EOum4ZDWc+Swq0Flz?lXyqr(W!5oRSN~Aa%oG|oy-EyvIj&jbb zF!>9MJ1U5F;gPCPd^OZ7d`LM0x6oCXjTlJ|Ji!4`Mi$ETJ|rMX+YC7DKVwWRBEvtV zpR`8w*fsxJe{aWO{}GQR;IJ*iw`f68e(b#p2>!IHrc@DV9)nm~tF8yE$QQ1y?e2TD z`^f6ZFmZO7+TwfDASj#%hfdRdUJ7c>4#n*UPT zdugqq?NP(t)rP$fE@c`HX$^-mB|C{A8V|5`D2J_)qFsBFix^F(cSvxn)f$nd<~ipOlt%8 zOFe13M*#Bicm;-N(Gq;^mTOi)I+x|0iC0|kv$bLs*g|dSXIr4! z!x_SJ*DkE1?TL#?QW|(wZY>ABCbZRjOn`#{wd#w2_OlW7(mmSrN$`A|Q^F z20b)`e_aNNXb5nb(I>qO2!#Sv5ESbu7p58LfjnN5nl8@Or2G^E+|pZV z03%8@_Z@*J@r?3FlxZ0*G6cjvv6uylm86>mUV(+MVYiuI9Ztcq*Cmg|5gJn1?7(r3 zD!zi&UZbNVXqXW3hlj~Ts?9XIkTXt_H7P+5f#@zq=-Xj~3K7MqO$mh<0|d$Bm6O`{w7{uE zP~6^SOX2t~SS}abTP-XB9dn?W+G6$cHx*UiEuwi|T^5y)#j+AGIq1%1hyX!}hci~! zh;C=VHoH<>gMErkrxZ+V|6t*tioD!o#A|Fjw0}@tY{U zg$Ad60pMV%hY~=bjmk;9+fU3p0g9?Hroms0e(U^yc1-S;Wj}3Ru z4rN9Hq~_NfCaec6oWP}vLl#i5aiE-UYrDOjBU$f!0`BTyQRd;KlH7#Xod^K{8v}-7 zdQ)I&fHa6kGKS@=HsF>m6qX3H^x$)PPz>u(GU~{d;1CzMSp*Ozi*ijzRWE@ALox>9 z;V&pxIi6ZM(rkrcU@}e_lhsT-*dMMDI3QnV5D?oO-nymW2g4api{^n5fw%Mpgulee z+Z0>Y{Xr;G-lUZ`J$97g`FeHTQsSM1_aNNawpL!1dW{~$-@EbdjdvFAExdE|-qEyo z*Lr!)(#7SYneq;;yyLN>2>Eb!cg?nY$5!?}YTUcpxHr={pfwI;s`qQv`_tb2Ux))( z-RbUUKda05pV$1)r@hbT0}%o!Ou`7QJn;ut@km^7t)^g$&g8JM16V6h$O*$BmLHb? z8RFGU7eX@A8KeOp)IBxK#RQ=kX(4ppKX0Tfd4R`ZKqkve*78{AKK^SO?JAT6;9G)F zvP+43Z-QLYX*HW^)YJP2F#3c(+PZ~qnthfIYES5+T7&f?dkDRgIC9apo+|a;QAc@2 z$zICD`r~ixz1#$%bZf>mH?Z~6KnEnK$mKMxjLTEOeXSLkLqdeIKE{7>QAAxK+r}{w zuUX~Z1hhZNF}ALc?d=|B>wj3>Tt|I`SyO~bb`J=6vynufMKr~Mzrq4DHi(HLXj2HcHWlZXbNFPYo=r3Wv$*gaOlD^g zhdz!NR&e={`-^PJ+3|yE-hh$}sj)5!2?+*}a?MVgRJCqUM@h+pm_*S8%vnnJA_ncCVn%4R|f5dLhyYLrbxv9-W1q0L4_Nz;)^f}Za)+z^Um3nnEj3X4H1 zM07|}$(FGZ2%9=#P8H!zF}!GeAU+um3*|Nrqnr&Bx5Irk)!5eJ)Bs|2#+A#)q1*Oi z!|5OPU7f$nvNT*U4B+hcA7;o~}SX+%79h_n`-yjWS(rYChOb?S?n9n16gpH0{7&D8AGYWAj1t=DXS@4~wmKH9Z%;wQ}? zHs8mY7CjmzUaQ{x-jR2YymRc{F_Ji<#Jcu$$BUV|QLSz?T{*fAd&2oUgLk2cUG?lt zdv>n-o6{}BpPkOMShUT)nv^#mvm5w;%qa;Utl%^M%7uV{vW^u4;#z9sK${ra3{L1IdXJe0pnhUQ z(%#&3REC9Y&*krJ{rWFW#2dp$jW}*Jn_K~SD^D2U9g&s!q+E$UB!ghfPtX%Q5_vUD zvU3-Li}6l`PUGer+(MWf0`4G8L=hTTn8RadD~Dz|YB|Q;0{y;K647WlV4y89u@Vah zjaQ*Y!J1)NlF>b*cN@7yNUwqZr%B#t#5DN!GZq%{d*4DrIP zzrU}))jC2)^xS)UkD9C|&5-o_AZHiyW#S0W6hDhm9vAq8j%?U6xN-m>+J;_lj`2_! zi`UUc5iEhl2&@+ZX*$|Yt)<%)Y-|`8=|iCQ;?%TXvvpl~to$+;RzV391Ji3P13H>64 zC}5G}tJ8S;-DNOw@}SHKqXeT85YZM7OP8IjC8HBl=Ri8hT*-^6Swu<#|9siyInXka zQpo57%&-fr(v@5wZ)X4~Mx-f!F=HpfjMO4k$vs|l(JiFi-0Q-;Ub$g}%dO@2#kuco zhOjPcL`i$wf~85U8$=_fXr%H2`Y6On3uQ_Ce)9PU!Gd}{1N>UK6f&YF^*mp_f{j`%_BeuWYs-M%e8~sK)*`7g zh4euP1CaE>Q4V5W=1h5K>YxmM`ljo>1sF0dE4j)+8>7zNdQUYgGUPZCvjTQYicu1b zMl+L}WEF-%r(|CQXxNtf)>`dIQd+9A?c^X#`Qe|pAdRD@0*H$r{F3xC$p#RS05BUR z>&GGAobW&6`jQ;6*~&JIUqATLLz%Y<3UtHb)h$>!?G+Y!(?MW}bYH@Ig#^Pe&!i~B z^H{WGI&{gJ@e?La-|jx+W|zixBhx zPRqhRiwNQTpoZ#BG%#5qBqKU3e1Tx$5MDq4RY`^ttsfoC__{P-SGwd3{q>y5l$_D{ zJ5Fj@-u3ok3jfmE58YY3i!U%0)M}ekWow>VSl6w09(Y*ui_M?z{o@0f&huI)9bJ?< zyIiByx35)gUE1=&`TLtP+xoO^eVM9$t*RgD**Zn5>(uHFq&({gG&peQ&AV?dU&F~} zY4NukA#mF@{thK9>^!fm`joWzYd9^UtmEf;l>&0Q;KMDMs!pw{^L`xVgZi$7-Qdd9 z{mT!I|NP~Zn6`5uQ@LNO-2c$2RSrHJ%Tzv_DntN8h}T(B42!eW4^lsP8~>M2&|UmX zD@W+SwbT#RE31}T-YLCTx_o)%((-ee%H8*;?!R>Z<@+ydK3I$l2y&VryM7UbqLuX3 zN!*0{$_l>aU%BzlO}h~YPY?rP(IuT-oPm1)!^R#OviQ3QN(5mV_)G!aP=lwNoIb}z zj92MSD6Ba6C0lvuQ74EPU^XA_X~ch;=Vw4P01k~$3lbnjdXiemUFKKE)kk zB^+z<=x8nrMPc+}m`*ac#uz8xI7kuLbzbj??qD09n(QV6Y4S)rgGe&nA`Tm3xp1H~ zD;Q%JI{}C6#P~(#I)zLG@1X-k%kp!I?%1=AN`n>9Ml^b)WV2J;lF)~44K_63iS;-i zWEm)pXY%GGzv|as>%bqm656XjPL9$z4@8DRL+!#dk4A^cwV17rys6z#NoOa|FVI+6 z$#W3RQWyc21?NWNICdJoQ>q$MElPs+f~?1~&eUbBa*%0%fe*o0-=S}0Ii5qN6^RK< zdU#mG4@x5K2q-FdS1SIi*q1N@_O)pA*LnX^+II+l4}#(|U2;flzamtiUnKWqLW_9Q zr*N~m{3M8-CvdayJ+qTW@YyI>bQ7t7gOCbu<8(a`69yRt`x*p>egYSZKqJYrT)!xL z={N}KX}M8r4ml%Q0w0zY3}F5RNqsSVUN9ymZ+xp1ek+9*Vk)25qW4VvsatJIx56OT z2eQW)^(6X^qpvXDALGBc;CW;Ta{~Un6l0ciAOuL5;=uEuo(!PC>rm`5L9_&>NstA7 zlg@RUvUPI@j)np%Y<5NHCg#a*Ao_C*2Q!E$%q>b_N;e}wAgAjHDJJ-R6KEk9Kns}! zAY#!ZQz7g{hzLTnh=+!-#~_=6A%tmTe;andQ36qt*i-BwC4LRuAqo-FLWbs22Y@Sr zO$ZOBNyFKj=8VRj#WehdAw3=bgU}oy=p&5?2qem-$?34piWnAj10rNvHulLmgLO0& zKukW+bcjj@Avf?FgbMtfW}JjWXhED8Auk%TFJat5v!`DdU}QpP?$IjIRll}KX%iD1 zqY0?k2-?S@RfudTR0l>6w4Sk3s94k*rGPLX`gsxNs?v1s< z8wFxpAf1%ukWZRehjX&R_4_&l0f5zQQm{Gkx-fveg&1vMNx-`n^BqO2Sz5rF zRfg!SU88QoeH5FU&@zOlVn&RH>B0~Wfx`q%PIWH|og4b8V@#6ca!Qi~r?7|Syl;e1 zB+p?nu3-ZhVcgdyXBm(i_z6;SJ1TxrC>mkni**gmA_m;rb)4}4;XHhvu(R}GwhZvM zip~??BJehbix!iRL}L=6UKUNE{l?gy(W?kql9z?*=;n!O(?!jl2o)rR<^3B8%XyIX z9dKmt{yqJ@`}ghVF*4-j{Rh!=kdqX;i43%k3~Veomkp)RO1jjFLo@E_CH$@grejJQ zMFIxC2VV<&>Y$`A>_U7tVzhLmx3_oi!T#32OGgI&ZXMB&=oTGVho zX@Lo-@br%CAL!k4;NVj_auGNoL}Zm6HqwH~RXhxfQ^YSCJm?^H7wBjR1cU5V$E(0t zAlV|(z!9|(3<}a<~bFXT4Umko5@Q$;vGFq9+1sBI| z1o7B1Kn>UZE5tVcCp?5^<8r}9s*MuwqmnJFC3G&xflNuKR??X+>HNHI%ksr{u91^8 z&RyKOwsm{z{CZ{e()pF9M-BT|8}>i+d^(jma6Z#;L2I~>sT|QNM^c42{OVCf<7!3Y z%DD$EnWh7oii29k!H35)6-RKcND0o;c;s(b^*5~SyWjkvI^*A``SwzLvE3 zYr`>E#M0yE=g_f4Ry_@APXkVMz8`t`LZPLP+B@<$UPmP? z?U!%dci*4N`1WbOeQ?(j+`onMb`Vd>+&>NYQ@H;ednM?yO${}^LG=Lz#x<0IX|1r@ zMCFR_ZK{^SMK|I^Fu8TZRJH(e1xN~n>HtQq`K9D= z+j*rFp_HPaD_PDm3ZtwCEl=x9yE|~hDrl4ux(F6C3V?yN^n1LwG_F_e- zP@HlA$iuJo@ni=I$z4i%iF;tD-_RJEE}{g1XwW*5!#JISpz7eaQALzUzn(%AVLgx* zh3O)(FsySamxaA|w(+no1~V2WYk)>X3=p@v9eYnD>pa$As1(_yz1RX(2k8BN+_EM5 zfT;>SBgJr>HjM0u@Ez;N(_|$J8!ce_KN9HtACVTIv)55o`>3*MwX$jD5~TcE<>IG&27W;(WM-QSq;H*2u4@itS;iKD5KwaU%uN@eBD{rT0Vz3HaC zf6)4A^Jkks+nj!JEHnJ7HvDR4@HK7lHN07?-kN$23W(~>fQuEi%dWShspz_|ZfSmb zW_f1iMrKEkwxdTJI!LBIJJ&YtSgKg_Rxf?;&iT}{kBc49$Z#gu_4<@d#V)PFIH$>G z=ZVeiZb~#Rcn5<2z)d&V+H09weCn zHy^WBAmGxvB&|EuDuGP{eqtXp(Mf)sPM^8)H zUKIFOR#ABCF8W2<-^82?k}{w@OIG+*tyR~Nx#db-rn*h5 zZcBUHzNo^Xd-uE2RR=Ov2ehgK>O7nu)wfrJmNyA0KYU81CZfVvhql;nFk5k0fY|(5e0reJ=@`e;$q-=<_@E24d z*_ULCM96xZX|;L`Z=`je4g)UGPJR>F*d+=fsH#^^r7Joz6&+ed$D@kg)r#IsMZZ?j zpK`5N)h{=`v;E$7-hl{%HN5I+PJ5cy{I!q#Evx>PbgLPF3Lu~q%%pR2e)g!UQux?})3Z5eQA=_%| z4srh;BV~i!r)jc3eub(fJ zU{c*|o2 z&u1a4u^ucAiOaX4!7_V0e?&4Jn1Y5mO7T@P`{Ik9EV{{jOPX+blmFTP%%sRy={!Rn z`a!HBMJ-)jMk=DrNv}(gm4p=mk_XU%kHBoqJOJtC(aHkc=2Cj|YJ}l4>d`T$CFjQc zI=Fiyt>mPYZ=5~Mo{uEU^^>4EDw&9L<}=hL;&e0h8w^Eqa}Zv+bP%^SjC&h^35V}W z?jR}_@A*^{8S_h?_y%27Ts@64+Pzsf9r~dX-6*UlVM60jPXV;3Df)JWZj8p>rEkQ{ zWeedspe|&q#sk5?TtY`^IKm5JgukFv!<7A(^o?R%A}%_`W{->WsvqJ>z7XLI!7}N8 znS=G_2$=nMcn?X!Hb>Q_dmfe^Jo0Q^^=w@p%6JsbqY#03;G@y|wLcmAaO}aZpT4p3 z#(K@x-iPauxt~DFM-5nlz)bKNYxP*F$g!fdEmXV&eha2&NXCvp50*I%rVH zI`Bz0SV|N|+{6XzN@E5@$t^luYK008fUVWk^>dUD*d`h~UPS&0V$3YjZ~q)Wlf8p5 zU)QCIW(kNBgAhsc1ib-+q1Gef2pt$y5Z;P`5kVg^>NbdH^hu?~Bx3y5k_jtf{Jw}X zpp}&&e%nh0JmQA^3MJ;4yJXLuEyalqIOHQS6J_O6wvf-X69$z;=IDbXRYNZlfU0AO zK_1xq2|=hoLmogV7=72KO9tt$ZZK0asPT6hHS3hqLP{gF$VttAGVMLN?yFvIzF+j< z^rr{XzVjL1dChkoZZjT^L3zTtwq1F&y=QfM&x4|eC5YLnZ9kqmxwQGt$eIU{Z=I!0 zYg-!ep!UwW?~(EgwY0-ExX>Sw8qV zBK3L%C5Ry~F4|j63AW&iovo(upug$-AcwBQ8ZJm4kc*Phu7Q~KGqK1Pfvf|_SRyDW zYgi%2i3Lm6NwRm0@^zohvhcN4Hg6wNS| zd~p52_4JlwIWe@DVv#*Y7SUe{Y4M(;G$(kAopu>@fJ(H$8c>P#OeI>L$#U_5@!HU{ zU8fZ>ZjBp{8}H*EMM!a$||Dp+$r)IBdQ#ZX||Am;pJ>JSC!@TS?1>FJg<-<7fvVzq~6~I+N25pfY84lq%+)|V67 zA86Vwe6Ii$$^w544#>bohCcv@+afzYT^K9m?u^Pk=?y(kdbYk49g=3^jQfNVp z+gXD1fA2G^3Gw^`om)*s(~v^!@DFfywJ{gm1IuyA9UV+Uz&sFhX{ABv%PQ|8542#6S=LytQ7Hk8pDtR_2j=@TrT%{o;yvw@ZK_VV zl7Gud+fSKy!FE1j;#o;EAwzHdEvHpV;+~{~>LANI9FNSKk?cH424^fU{nQO8&0s*L zPV+M4)?ne$inAU2Y=(hyxBgYxiXs2h8nAWxSy zLB3rc3bi$>RT8McH`M%|+&Yd4_UQ;kP^18g6hD!|C$@-{bmXb^x0=EwXf-fp&miGn;@@pYdgmMX`=7UbR{dG%Bj-x%Ps%>x*NZT`%?mC%KPKtEaJ2P>~olL%r6Zv8@5r-Z<6rD>fBx`aOaC7`va~yhT zW}=yic#OuV)H^WbqS@l~#kgRv$zU>DL}5H&Ysn*V9#0U1f(=#$iuwVKWM&ccGo$Gq_Ch)a!#60c_buWA0* z(%#qB$U?JzwWNNzIa8wif9+jsY+T26zWcc3a(BsHKJ_q3Q4&d!7DbB|^|IbC%93I_ zi4@yq9yG9tmREjBGv5BI~y?)#11`Nm$Wu~!zrJX_?H#B1vYeXdbl?VbT6la<_LCO4q~t!=rGk!1CY(u5je|z+1Wk&z1c;E}~KQRrAAl_L=F8W@>;c zr_}8oxoh_m9m)~VTPJRvn15;^bNAfuUHbZ^eD5}^cbmC#dp^0tO71X|JD7gg{95SY z&BRl9J0$(e_qHrtz5B)d`YqP_E#}JMd~&Om+-fGbs?_TM>o61Tk529Bo&5dK)x#Kg zfS}%qi^vsjbB`JNn<>WY>d0UIXclyy?Zj&wrI9bNi^s;c1nS|;%VhuuICs4N2){n85qe70~-X=MKpMf!8 zKgR%NqC`~u%%_*H|FcLA?Ps6X)LxFgN;SRp#;rHLcIwX9Voko|8LPua%$l*ym4E>E zb^Hu6B#*U#&&uQV(EDW?H}GJ-slx-pg5OdLIEekWn*uMMKGBJXaqHOa^ORkDVpA_g zAGHPyC^wcyMOIPuCoi6d?XEPgkIhh7+YO`QRz1^}Dl=|4q6Ov&+63}ASz3|6Sigkn z2)uv-zYM%()r>>>O3A09K*{B4$Ri58DsH?3&nl(z%3S#W7)Jh#SV@E4{68TBq z@8M05@`2s2Ob>G7MieYf2;c)Q8Tuy*B-5Z^`_Jpz!yw7mq$dL5@xo63=& zGlPHRXVMq=uF@%ujQlW8)%#wz=f!K^;nsdQ$pjPgz<ED{&UBPsU41_b^`BC1uD*lRO2hJFewzfDS?R(Rpc2I$HeiQ{xK=iX(FK7j7~wn@ zxpMhRF-^b0oY}^0g<3J`|7CmuDGg%b9rH^>Vk)1#5njO#gjv!C3K&T3tUD`wl>3%W z@%^hoyv&v9=AfZWGuEe0Yy^Gl+^uuD?EGQ11!*oKUT33q>)~4Qkk!8Ve%ofVZS%cD zAFQ{YJAQxoxH){>du5w8d~cF}X5Yzt(wQZ~}G$2mc`r%KO<6?wf4aAfDi*drV zLWg0e;n?x;3NkKZcYOMlDiSTMX?dfYyS*Up55-s%73`63q{`^F{(~vq@yi*t5ay19 znOa{;HxwyjcAoB7W-u@J7g7lJ6Je+jb;yX^@p>d^u_`z&d8zID>Vo2m%iTh=Y2@eb zA>)GcWuQ+#Pj*ob!T^i}(h#^P2g;G;wU<~r&N^}+v|e)LxYiKdjr(N61e^coDMh$4 zjlEcDZwv}@fG?keIX*0dLY#3tVY%++?F%ZlIz7FFQ7D>q#(YJyH=Q3nC0 zxtfT^wpSd!xQTE?G>C)eC%gKT0B28n+``r`!Xi=rKDeE60kP}F?e z85UFnda$}t!=35+lfoPt*O8U7k17}ah?jdBcA2RDj$vOV`4%5Y>o|V$chJfN*iM+*jyWduI%ZHvsfz`=O*xK3vz6JM&(2;S?7^ ztgfN^Z9`_;(1XryC*P*IeYW%ULR-7pwq>EsTDxWLsCc}O)NL$uuCqEftHg6J!a?Po zefWWrmg0c%*#`alYewbhkBEr7io;YAo(N+wkN?YAy#>%i;zW8s^>Vrd8Wfx@@F3} zl0D>I{7Eag1_8Zdq0-=PaM@ERdy$08G6?QA2%uC@P6Et~49Nt^ev->1qRk@zq*yL$ zD2nyta3z6@?=9pU$Fds=rcrB&5emn$WSZ z*1BT$1#l!A`poDCGuo1S9d(vs)w84M2F)vGPvDA% z6cW{FiAE$LbaM6x19@d!Ye?gxNlUfN9>uSk#=_9n!ocQI1ZCQ#3t7$5QYxP3_-+7c-Hr@P; z9nmCdbR91gh#SX%20IGgxpjS)FKQP6_caYHB(QxTjB2>~iV8@={l?8P=Dv-0-NwkY z6R=XQOR@q$-0K*CESoK47>?_G@V=$@F?O{Z(q-TA0G=hP9|*bE@&{^0l?U5UqxyPZ z-1<_oOK&b|LGrPXRPrkQ6UdE}S4bO3&^F{D87Y!<8iPOC3N_88aHrl{(m;4+^Ucqy zfU(e8{h5*$BqO1AeS1lB$zgT#R(*F#195N7fWcQothNezIP$ci`WV^RF;_=h=}o7!0IR4fy{S%$Y9t3#+> z5gMOLWNIoxlL!TB^BcI=w|Q4N#_BUMp^H`&8#`@WJbaG5D^rx&p52f(LnZ&i&9?pOhaJf?UQy8jcL zcEzX`;T6zy$YMY+v`o`MwgSH00T&U2VT0r2sI#l%<3AvS*nvo>IpSLDD!2i!E3~f0 zM;r!M@R6LupS&;u%nmC~DsMl&P)YcCe5zx?tu`lwpGe`nUu75`hsrWOo8f9P13U8wCh9o9o9#MyY4vh#kvajnqUjA zTLoU|k5~!qC?qNYc#W3ZH-4DvhaLXK?)L}ssqI$E+hFT|Hg+bdgAsgr(mKe5W^h$d zxR!3UrLrb~%^^f#={k_@h{mr~d3a+8flzU|Pwji*{vMD0aBcO+{-81c^mF;tE(?~% z9rNP;XUC=L*4{&@8u%A&hNB#~jk9_}yt8K`NLI2BgIlGJ#4k?O`Lvg67*t*@TlK)iWcwn^$4 z8b_={4qHzF>Ef9raBB)_+*faRMZsW{Aan_if!@Hz5Pu>49TAQtA~ZgLt_1;5yz#~C zu9KPBQ0Y&qCmZx7)syMT?!`bXRWAcEpBU1mzraM5gkA(B1kUwTI{|dim-9JhOkl9FVY0Qde|sC>#v{JV!UT|K;upepQO1ngi0>|k`PHd7C_ zFEkdLs;k22#SvP`=p_&eR_`0g+Z+7Gk_ZLsR~A~2(0D~?Lna|iViGhhtYU*PgzP33 zQF38J9>FS#8Ny1RC|D*rf93{!ViP}6X?s_4emu6)=Gl2Sf!k}aDC!N7AD1DDt^M#C3Cu(0^5tJXxc{PjLJ+cI=D%9aRQy#oq6`k z%lG)qIgDLs<1U{;R^9j&jA>w!PjF}x2GKP|>*!XiM3w7SJqG#(acbZ&(7iyfXS-2I zKLf2eXP}*hoo;K#M`8|tl`||$%KwM^L4q^v|A>#??zZ!xfH_j94-0mUO)9*$CssI8 zMJp>w>Zg4APCHpRuffb}G8d{g%HuY(R&0(M>RCTW-JeelTYx1v&;#7iJ=|9BLQt?a z{8Kr3M3EYP(|uaHr?^PV^t3$<4=C814krcj)`3C1dYuN!okEI~1uKHn*b*jVAVk~p zAw{}qSD)>3Yj{uNQ4kkP>|4aA!GV8Fs`fBac%rttnW(RF&Whtf% zdMs4d>+>0_b4xz8#f)vK>@rc*jdU4K)4@u5beE|vb{N0TB0oZYa9i3D*bk%J#+P{I z0hA;l9=Z*wcwRIfQmDh@c>NsN3?ijTZ@JYuI(_jp7SSO1IGn!SNX}jeC^ck=^2SwL z6gW?>GiRrPpTqhV8W&=;3`Mr1D4zjund@kl1?xn%HE&;g+LGmuP&Q+8wMMNE46!pS zyrvCA9Ag8wAs37husMLY!Ah-J=)SuV-sa5Mre&(H3l{~OIPe}IRx0h&ZlzTUkIFTH zJs5~@0%+(_DUV$XVZ|+0t$?^UM<~%+brLW1q_1ytAb8vde|&+%8_~;NIbmp(NT*8W ziW*}qRHL*9MUB_zPu=+?+T@P)RyKt@Ko2k56#l+^1{=N!bhIUm@aW7lB2q{xoRMZv z5Bep>VSWZW3j!!w_#TTr?+l1vo)AV|i4yk1+2X}#`D|V4*&sJ0=QDTCFSc0ya2U25 zn+8(*{{+5XT~sI5XiTK-Z(Lf2@CqV5fb*asf`kln@3L@8+~c=9Bufesks9*@z1t3)D z`Y=`7!(JRiu8~M_?!R%aill7mg@B$&N4zlRyWW~G~ zA0Iz6OAMAbx`;nZD@*eZ*oA6fOwbH!;Egm#ksMRjqE?^N`o?=s0Q?mQZWKI?uv8$B7?)mR51yQZ?H2oDDW>a?I||VWILn* z9ugO;6vddWzt4)9qGJ0bp*Coci7V%B44#{qK|>AFEe15PS%W48jrV!^P_=Y0HhB>~ zDz5=XGI<`J=#1>3YrZI3O!_Y=4)}jLH>#4sMQriT7CVR~A z&%o5_>9fUJyVjKvri9Z5a0z6jcww4^v!ZzMDAqd(q>mpf8vW{duNjFIkNiWF;kYaP7G=nr3Z76gD z=b~b(6Rm1!PeP zpDfi&QmX$O^uZF{yr4aAE`1iXUNd->q78{~Npr|-Eg%TdS}W3=J9|IUVMaO%(e-BV zEYzmw<7VwToQ2q`xwwUm>;3syw-xI)W8H;V;;p({b!HQsh##|J$IRHVLaYg3j`|gI z2X7zGeIfUS`PFZ~@y;6yr|)L+$&FTW<80l7RNLJ3x$CpZQnWDy>u)aUtqmPUtyj?1 z%^|`zd~{d?UOSW9m2X&MHLRIUfOAf?&aJx*Gmu01__J30Sv*^GOb-FNQk~{%K9rb` z{PD*VTDoKI+U+;y;k9U;#d8CSiOu+Ha&7N)%wNqnc3V6*&Nd*lVfAdh6ur_E0v@9} zo!j=I#C+tBH+pu2prfxOx%v+o`jHoP4bH(sgq1A)!HXpJqhO*}B3eh+V*9Q6Vd9*k(;iF6+9j(lf*2=u=JGSCA@ literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/__pycache__/win32.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/__pycache__/win32.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..df3fce1cd040f645d93d251148098916462c973a GIT binary patch literal 2539 zcma)7-)|H}9G|`4-Rlo1A_Aq1Rk${`dmu4DA;!`cN~BeLM3N)snA@H9*6z)oGqb&3 zQ<6StA_gPzg?kV~jG_chc<>MCgKx&9H8nPyn3(v)+ZptQC%?1XYp?!Doa|1%yWj78 zzCY$WpZT(*Lq{+^o&9$9Fi5|tkLHMN3XhjTxQ#57N0w$q9IYJ5M>G^cG-5?*^j-v^ z2k?eNQ#LUtR*vW6fgE=d3mv4v&c$)rsdnMwtNRozunnH z%6D3wR{OhJzRT*!p@A-vjv`bcLO6EOsM_qri+Bi6=I|ApS#DKe;Z-Uwm+*6J{PpSa z(UHsJV}tn0#Y>m)xp6!_KKa_4<74>Z6h3a8(sfy)_|oLWNNxfXX5p#Jq3F!I+?J)X zz=Tum32I@R;d#Ps*Ap1L2Cg`Dhy<1CGD#Pt7}SfN#Iux9P8_F(nJd*Yg3E-#Kd>uT zh_s6i1qfNfCAW$S%|5srC>x&NLt36zT?>(+^WDg3#GO{sO=nc zs|**pJ4e}IuuAnypr$n!M4Jg}!scO@DFB;eSP(`{Jn1%*9UjJO9Y?hRr)76OZ1QG9!tpqRD-JYBP)U{3HgHP_nE+_ZWuOVO%V5)ia|#88+DJ+) z1Hw#}=z;^it3;34YFj|uZBbF6%v5bvC_1h=2R{>GjssDigcN;!yKbQzsznNvuU^PK z`^t0AAMxWvn6`c11diAm36RwH<0VZIn1s1GmXO?{EQEGa7~i{u?zU)~YYs zTe~xlzpaZ|+W%V@c#SPZc%SUr#$Jl;jPb52|38Km-)Y6Pr5KQRV(~55WOWi%msMSE z!68*F2HQVWgif=z$hgVCXP`HM6M-av9+fGu8Z<&kaf296_huJiXsoG6>e_f5Y|JCo zG+bS418u%;gBu|UGbx9HB0mwv;m6^&pxlo-lyTK3e(KU>VRGcc#Zj*AJwIX5phe?; z0**I?;-`X@5Q!fJkA8#+Kk9l?C|E@%_{oKA$t}~YC=-vnS)e>COPEl!pM^UsTdB#C z%T5@_k7tD~=}?83b7Yo^?B>bK2B+4j)VL~bFueqV_#CEX^k*kZb>BLAC$pC9TTk{i zkQN`<*uC%ep^p#M7uI(7ukY@EKe5r)^-<3UJvVx9_SU2I?s{~+t*2i7s_&cJ!>8|G z{rc*CW9{(iwYJmC=YG)-*3Z@XU9HYPIse7Pnm)9y5B+p(WTkKAjg{$@>F-!2=0I@A+h@@bz`X`9G3s7CX;#6c7zdLpy3X7 zH&p>?xCXRPxP$FYg`%R6Iv@hLkP8LA2mEq%;)1f?NBp3Y55V*!bvC@CUgk&OQ?-!z z9;QZA)3iopuNG?{H5o`du!{D#J{u^u4BAM-Xe3bLnctI9ZT}w#rbaMzBkj;C>S=wz zP8(Q7{jJXiI<~s=*+7R^cRr0+EFEc}=Cqt_>_YAPmQ%lW>{;*VUDg}1w05e2{$mP~ F{R{k&#KQmp literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/_data/wordsets/bip39.txt b/ansible/lib/python3.11/site-packages/passlib/_data/wordsets/bip39.txt new file mode 100644 index 000000000..e29842e6c --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/_data/wordsets/bip39.txt @@ -0,0 +1,2049 @@ +abandon +ability +able +about +above +absent +absorb +abstract +absurd +abuse +access +accident +account +accuse +achieve +acid +acoustic +acquire +across +act +action +actor +actress +actual +adapt +add +addict +address +adjust +admit +adult +advance +advice +aerobic +affair +afford +afraid +again +age +agent +agree +ahead +aim +air +airport +aisle +alarm +album +alcohol +alert +alien +all +alley +allow +almost +alone +alpha +already +also +alter +always +amateur +amazing +among +amount +amused +analyst +anchor +ancient +anger +angle +angry +animal +ankle +announce +annual +another +answer +antenna +antique +anxiety +any +apart +apology +appear +apple +approve +april +arch +arctic +area +arena +argue +arm +armed +armor +army +around +arrange +arrest +arrive +arrow +art +artefact +artist +artwork +ask +aspect +assault +asset +assist +assume +asthma +athlete +atom +attack +attend +attitude +attract +auction +audit +august +aunt +author +auto +autumn +average +avocado +avoid +awake +aware +away +awesome +awful +awkward +axis +baby +bachelor +bacon +badge +bag +balance +balcony +ball +bamboo +banana +banner +bar +barely +bargain +barrel +base +basic +basket +battle +beach +bean +beauty +because +become +beef +before +begin +behave +behind +believe +below +belt +bench +benefit +best +betray +better +between +beyond +bicycle +bid +bike +bind +biology +bird +birth +bitter +black +blade +blame +blanket +blast +bleak +bless +blind +blood +blossom +blouse +blue +blur +blush +board +boat +body +boil +bomb +bone +bonus +book +boost +border +boring +borrow +boss +bottom +bounce +box +boy +bracket +brain +brand +brass +brave +bread +breeze +brick +bridge +brief +bright +bring +brisk +broccoli +broken +bronze +broom +brother +brown +brush +bubble +buddy +budget +buffalo +build +bulb +bulk +bullet +bundle +bunker +burden +burger +burst +bus +business +busy +butter +buyer +buzz +cabbage +cabin +cable +cactus +cage +cake +call +calm +camera +camp +can +canal +cancel +candy +cannon +canoe +canvas +canyon +capable +capital +captain +car +carbon +card +cargo +carpet +carry +cart +case +cash +casino +castle +casual +cat +catalog +catch +category +cattle +caught +cause +caution +cave +ceiling +celery +cement +census +century +cereal +certain +chair +chalk +champion +change +chaos +chapter +charge +chase +chat +cheap +check +cheese +chef +cherry +chest +chicken +chief +child +chimney +choice +choose +chronic +chuckle +chunk +churn +cigar +cinnamon +circle +citizen +city +civil +claim +clap +clarify +claw +clay +clean +clerk +clever +click +client +cliff +climb +clinic +clip +clock +clog +close +cloth +cloud +clown +club +clump +cluster +clutch +coach +coast +coconut +code +coffee +coil +coin +collect +color +column +combine +come +comfort +comic +common +company +concert +conduct +confirm +congress +connect +consider +control +convince +cook +cool +copper +copy +coral +core +corn +correct +cost +cotton +couch +country +couple +course +cousin +cover +coyote +crack +cradle +craft +cram +crane +crash +crater +crawl +crazy +cream +credit +creek +crew +cricket +crime +crisp +critic +crop +cross +crouch +crowd +crucial +cruel +cruise +crumble +crunch +crush +cry +crystal +cube +culture +cup +cupboard +curious +current +curtain +curve +cushion +custom +cute +cycle +dad +damage +damp +dance +danger +daring +dash +daughter +dawn +day +deal +debate +debris +decade +december +decide +decline +decorate +decrease +deer +defense +define +defy +degree +delay +deliver +demand +demise +denial +dentist +deny +depart +depend +deposit +depth +deputy +derive +describe +desert +design +desk +despair +destroy +detail +detect +develop +device +devote +diagram +dial +diamond +diary +dice +diesel +diet +differ +digital +dignity +dilemma +dinner +dinosaur +direct +dirt +disagree +discover +disease +dish +dismiss +disorder +display +distance +divert +divide +divorce +dizzy +doctor +document +dog +doll +dolphin +domain +donate +donkey +donor +door +dose +double +dove +draft +dragon +drama +drastic +draw +dream +dress +drift +drill +drink +drip +drive +drop +drum +dry +duck +dumb +dune +during +dust +dutch +duty +dwarf +dynamic +eager +eagle +early +earn +earth +easily +east +easy +echo +ecology +economy +edge +edit +educate +effort +egg +eight +either +elbow +elder +electric +elegant +element +elephant +elevator +elite +else +embark +embody +embrace +emerge +emotion +employ +empower +empty +enable +enact +end +endless +endorse +enemy +energy +enforce +engage +engine +enhance +enjoy +enlist +enough +enrich +enroll +ensure +enter +entire +entry +envelope +episode +equal +equip +era +erase +erode +erosion +error +erupt +escape +essay +essence +estate +eternal +ethics +evidence +evil +evoke +evolve +exact +example +excess +exchange +excite +exclude +excuse +execute +exercise +exhaust +exhibit +exile +exist +exit +exotic +expand +expect +expire +explain +expose +express +extend +extra +eye +eyebrow +fabric +face +faculty +fade +faint +faith +fall +false +fame +family +famous +fan +fancy +fantasy +farm +fashion +fat +fatal +father +fatigue +fault +favorite +feature +february +federal +fee +feed +feel +female +fence +festival +fetch +fever +few +fiber +fiction +field +figure +file +film +filter +final +find +fine +finger +finish +fire +firm +first +fiscal +fish +fit +fitness +fix +flag +flame +flash +flat +flavor +flee +flight +flip +float +flock +floor +flower +fluid +flush +fly +foam +focus +fog +foil +fold +follow +food +foot +force +forest +forget +fork +fortune +forum +forward +fossil +foster +found +fox +fragile +frame +frequent +fresh +friend +fringe +frog +front +frost +frown +frozen +fruit +fuel +fun +funny +furnace +fury +future +gadget +gain +galaxy +gallery +game +gap +garage +garbage +garden +garlic +garment +gas +gasp +gate +gather +gauge +gaze +general +genius +genre +gentle +genuine +gesture +ghost +giant +gift +giggle +ginger +giraffe +girl +give +glad +glance +glare +glass +glide +glimpse +globe +gloom +glory +glove +glow +glue +goat +goddess +gold +good +goose +gorilla +gospel +gossip +govern +gown +grab +grace +grain +grant +grape +grass +gravity +great +green +grid +grief +grit +grocery +group +grow +grunt +guard +guess +guide +guilt +guitar +gun +gym +habit +hair +half +hammer +hamster +hand +happy +harbor +hard +harsh +harvest +hat +have +hawk +hazard +head +health +heart +heavy +hedgehog +height +hello +helmet +help +hen +hero +hidden +high +hill +hint +hip +hire +history +hobby +hockey +hold +hole +holiday +hollow +home +honey +hood +hope +horn +horror +horse +hospital +host +hotel +hour +hover +hub +huge +human +humble +humor +hundred +hungry +hunt +hurdle +hurry +hurt +husband +hybrid +ice +icon +idea +identify +idle +ignore +ill +illegal +illness +image +imitate +immense +immune +impact +impose +improve +impulse +inch +include +income +increase +index +indicate +indoor +industry +infant +inflict +inform +inhale +inherit +initial +inject +injury +inmate +inner +innocent +input +inquiry +insane +insect +inside +inspire +install +intact +interest +into +invest +invite +involve +iron +island +isolate +issue +item +ivory +jacket +jaguar +jar +jazz +jealous +jeans +jelly +jewel +job +join +joke +journey +joy +judge +juice +jump +jungle +junior +junk +just +kangaroo +keen +keep +ketchup +key +kick +kid +kidney +kind +kingdom +kiss +kit +kitchen +kite +kitten +kiwi +knee +knife +knock +know +lab +label +labor +ladder +lady +lake +lamp +language +laptop +large +later +latin +laugh +laundry +lava +law +lawn +lawsuit +layer +lazy +leader +leaf +learn +leave +lecture +left +leg +legal +legend +leisure +lemon +lend +length +lens +leopard +lesson +letter +level +liar +liberty +library +license +life +lift +light +like +limb +limit +link +lion +liquid +list +little +live +lizard +load +loan +lobster +local +lock +logic +lonely +long +loop +lottery +loud +lounge +love +loyal +lucky +luggage +lumber +lunar +lunch +luxury +lyrics +machine +mad +magic +magnet +maid +mail +main +major +make +mammal +man +manage +mandate +mango +mansion +manual +maple +marble +march +margin +marine +market +marriage +mask +mass +master +match +material +math +matrix +matter +maximum +maze +meadow +mean +measure +meat +mechanic +medal +media +melody +melt +member +memory +mention +menu +mercy +merge +merit +merry +mesh +message +metal +method +middle +midnight +milk +million +mimic +mind +minimum +minor +minute +miracle +mirror +misery +miss +mistake +mix +mixed +mixture +mobile +model +modify +mom +moment +monitor +monkey +monster +month +moon +moral +more +morning +mosquito +mother +motion +motor +mountain +mouse +move +movie +much +muffin +mule +multiply +muscle +museum +mushroom +music +must +mutual +myself +mystery +myth +naive +name +napkin +narrow +nasty +nation +nature +near +neck +need +negative +neglect +neither +nephew +nerve +nest +net +network +neutral +never +news +next +nice +night +noble +noise +nominee +noodle +normal +north +nose +notable +note +nothing +notice +novel +now +nuclear +number +nurse +nut +oak +obey +object +oblige +obscure +observe +obtain +obvious +occur +ocean +october +odor +off +offer +office +often +oil +okay +old +olive +olympic +omit +once +one +onion +online +only +open +opera +opinion +oppose +option +orange +orbit +orchard +order +ordinary +organ +orient +original +orphan +ostrich +other +outdoor +outer +output +outside +oval +oven +over +own +owner +oxygen +oyster +ozone +pact +paddle +page +pair +palace +palm +panda +panel +panic +panther +paper +parade +parent +park +parrot +party +pass +patch +path +patient +patrol +pattern +pause +pave +payment +peace +peanut +pear +peasant +pelican +pen +penalty +pencil +people +pepper +perfect +permit +person +pet +phone +photo +phrase +physical +piano +picnic +picture +piece +pig +pigeon +pill +pilot +pink +pioneer +pipe +pistol +pitch +pizza +place +planet +plastic +plate +play +please +pledge +pluck +plug +plunge +poem +poet +point +polar +pole +police +pond +pony +pool +popular +portion +position +possible +post +potato +pottery +poverty +powder +power +practice +praise +predict +prefer +prepare +present +pretty +prevent +price +pride +primary +print +priority +prison +private +prize +problem +process +produce +profit +program +project +promote +proof +property +prosper +protect +proud +provide +public +pudding +pull +pulp +pulse +pumpkin +punch +pupil +puppy +purchase +purity +purpose +purse +push +put +puzzle +pyramid +quality +quantum +quarter +question +quick +quit +quiz +quote +rabbit +raccoon +race +rack +radar +radio +rail +rain +raise +rally +ramp +ranch +random +range +rapid +rare +rate +rather +raven +raw +razor +ready +real +reason +rebel +rebuild +recall +receive +recipe +record +recycle +reduce +reflect +reform +refuse +region +regret +regular +reject +relax +release +relief +rely +remain +remember +remind +remove +render +renew +rent +reopen +repair +repeat +replace +report +require +rescue +resemble +resist +resource +response +result +retire +retreat +return +reunion +reveal +review +reward +rhythm +rib +ribbon +rice +rich +ride +ridge +rifle +right +rigid +ring +riot +ripple +risk +ritual +rival +river +road +roast +robot +robust +rocket +romance +roof +rookie +room +rose +rotate +rough +round +route +royal +rubber +rude +rug +rule +run +runway +rural +sad +saddle +sadness +safe +sail +salad +salmon +salon +salt +salute +same +sample +sand +satisfy +satoshi +sauce +sausage +save +say +scale +scan +scare +scatter +scene +scheme +school +science +scissors +scorpion +scout +scrap +screen +script +scrub +sea +search +season +seat +second +secret +section +security +seed +seek +segment +select +sell +seminar +senior +sense +sentence +series +service +session +settle +setup +seven +shadow +shaft +shallow +share +shed +shell +sheriff +shield +shift +shine +ship +shiver +shock +shoe +shoot +shop +short +shoulder +shove +shrimp +shrug +shuffle +shy +sibling +sick +side +siege +sight +sign +silent +silk +silly +silver +similar +simple +since +sing +siren +sister +situate +six +size +skate +sketch +ski +skill +skin +skirt +skull +slab +slam +sleep +slender +slice +slide +slight +slim +slogan +slot +slow +slush +small +smart +smile +smoke +smooth +snack +snake +snap +sniff +snow +soap +soccer +social +sock +soda +soft +solar +soldier +solid +solution +solve +someone +song +soon +sorry +sort +soul +sound +soup +source +south +space +spare +spatial +spawn +speak +special +speed +spell +spend +sphere +spice +spider +spike +spin +spirit +split +spoil +sponsor +spoon +sport +spot +spray +spread +spring +spy +square +squeeze +squirrel +stable +stadium +staff +stage +stairs +stamp +stand +start +state +stay +steak +steel +stem +step +stereo +stick +still +sting +stock +stomach +stone +stool +story +stove +strategy +street +strike +strong +struggle +student +stuff +stumble +style +subject +submit +subway +success +such +sudden +suffer +sugar +suggest +suit +summer +sun +sunny +sunset +super +supply +supreme +sure +surface +surge +surprise +surround +survey +suspect +sustain +swallow +swamp +swap +swarm +swear +sweet +swift +swim +swing +switch +sword +symbol +symptom +syrup +system +table +tackle +tag +tail +talent +talk +tank +tape +target +task +taste +tattoo +taxi +teach +team +tell +ten +tenant +tennis +tent +term +test +text +thank +that +theme +then +theory +there +they +thing +this +thought +three +thrive +throw +thumb +thunder +ticket +tide +tiger +tilt +timber +time +tiny +tip +tired +tissue +title +toast +tobacco +today +toddler +toe +together +toilet +token +tomato +tomorrow +tone +tongue +tonight +tool +tooth +top +topic +topple +torch +tornado +tortoise +toss +total +tourist +toward +tower +town +toy +track +trade +traffic +tragic +train +transfer +trap +trash +travel +tray +treat +tree +trend +trial +tribe +trick +trigger +trim +trip +trophy +trouble +truck +true +truly +trumpet +trust +truth +try +tube +tuition +tumble +tuna +tunnel +turkey +turn +turtle +twelve +twenty +twice +twin +twist +two +type +typical +ugly +umbrella +unable +unaware +uncle +uncover +under +undo +unfair +unfold +unhappy +uniform +unique +unit +universe +unknown +unlock +until +unusual +unveil +update +upgrade +uphold +upon +upper +upset +urban +urge +usage +use +used +useful +useless +usual +utility +vacant +vacuum +vague +valid +valley +valve +van +vanish +vapor +various +vast +vault +vehicle +velvet +vendor +venture +venue +verb +verify +version +very +vessel +veteran +viable +vibrant +vicious +victory +video +view +village +vintage +violin +virtual +virus +visa +visit +visual +vital +vivid +vocal +voice +void +volcano +volume +vote +voyage +wage +wagon +wait +walk +wall +walnut +want +warfare +warm +warrior +wash +wasp +waste +water +wave +way +wealth +weapon +wear +weasel +weather +web +wedding +weekend +weird +welcome +west +wet +whale +what +wheat +wheel +when +where +whip +whisper +wide +width +wife +wild +will +win +window +wine +wing +wink +winner +winter +wire +wisdom +wise +wish +witness +wolf +woman +wonder +wood +wool +word +work +world +worry +worth +wrap +wreck +wrestle +wrist +write +wrong +yard +year +yellow +you +young +youth +zebra +zero +zone +zoo + diff --git a/ansible/lib/python3.11/site-packages/passlib/_data/wordsets/eff_long.txt b/ansible/lib/python3.11/site-packages/passlib/_data/wordsets/eff_long.txt new file mode 100644 index 000000000..caf71f526 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/_data/wordsets/eff_long.txt @@ -0,0 +1,7776 @@ +abacus +abdomen +abdominal +abide +abiding +ability +ablaze +able +abnormal +abrasion +abrasive +abreast +abridge +abroad +abruptly +absence +absentee +absently +absinthe +absolute +absolve +abstain +abstract +absurd +accent +acclaim +acclimate +accompany +account +accuracy +accurate +accustom +acetone +achiness +aching +acid +acorn +acquaint +acquire +acre +acrobat +acronym +acting +action +activate +activator +active +activism +activist +activity +actress +acts +acutely +acuteness +aeration +aerobics +aerosol +aerospace +afar +affair +affected +affecting +affection +affidavit +affiliate +affirm +affix +afflicted +affluent +afford +affront +aflame +afloat +aflutter +afoot +afraid +afterglow +afterlife +aftermath +aftermost +afternoon +aged +ageless +agency +agenda +agent +aggregate +aghast +agile +agility +aging +agnostic +agonize +agonizing +agony +agreeable +agreeably +agreed +agreeing +agreement +aground +ahead +ahoy +aide +aids +aim +ajar +alabaster +alarm +albatross +album +alfalfa +algebra +algorithm +alias +alibi +alienable +alienate +aliens +alike +alive +alkaline +alkalize +almanac +almighty +almost +aloe +aloft +aloha +alone +alongside +aloof +alphabet +alright +although +altitude +alto +aluminum +alumni +always +amaretto +amaze +amazingly +amber +ambiance +ambiguity +ambiguous +ambition +ambitious +ambulance +ambush +amendable +amendment +amends +amenity +amiable +amicably +amid +amigo +amino +amiss +ammonia +ammonium +amnesty +amniotic +among +amount +amperage +ample +amplifier +amplify +amply +amuck +amulet +amusable +amused +amusement +amuser +amusing +anaconda +anaerobic +anagram +anatomist +anatomy +anchor +anchovy +ancient +android +anemia +anemic +aneurism +anew +angelfish +angelic +anger +angled +angler +angles +angling +angrily +angriness +anguished +angular +animal +animate +animating +animation +animator +anime +animosity +ankle +annex +annotate +announcer +annoying +annually +annuity +anointer +another +answering +antacid +antarctic +anteater +antelope +antennae +anthem +anthill +anthology +antibody +antics +antidote +antihero +antiquely +antiques +antiquity +antirust +antitoxic +antitrust +antiviral +antivirus +antler +antonym +antsy +anvil +anybody +anyhow +anymore +anyone +anyplace +anything +anytime +anyway +anywhere +aorta +apache +apostle +appealing +appear +appease +appeasing +appendage +appendix +appetite +appetizer +applaud +applause +apple +appliance +applicant +applied +apply +appointee +appraisal +appraiser +apprehend +approach +approval +approve +apricot +april +apron +aptitude +aptly +aqua +aqueduct +arbitrary +arbitrate +ardently +area +arena +arguable +arguably +argue +arise +armadillo +armband +armchair +armed +armful +armhole +arming +armless +armoire +armored +armory +armrest +army +aroma +arose +around +arousal +arrange +array +arrest +arrival +arrive +arrogance +arrogant +arson +art +ascend +ascension +ascent +ascertain +ashamed +ashen +ashes +ashy +aside +askew +asleep +asparagus +aspect +aspirate +aspire +aspirin +astonish +astound +astride +astrology +astronaut +astronomy +astute +atlantic +atlas +atom +atonable +atop +atrium +atrocious +atrophy +attach +attain +attempt +attendant +attendee +attention +attentive +attest +attic +attire +attitude +attractor +attribute +atypical +auction +audacious +audacity +audible +audibly +audience +audio +audition +augmented +august +authentic +author +autism +autistic +autograph +automaker +automated +automatic +autopilot +available +avalanche +avatar +avenge +avenging +avenue +average +aversion +avert +aviation +aviator +avid +avoid +await +awaken +award +aware +awhile +awkward +awning +awoke +awry +axis +babble +babbling +babied +baboon +backache +backboard +backboned +backdrop +backed +backer +backfield +backfire +backhand +backing +backlands +backlash +backless +backlight +backlit +backlog +backpack +backpedal +backrest +backroom +backshift +backside +backslid +backspace +backspin +backstab +backstage +backtalk +backtrack +backup +backward +backwash +backwater +backyard +bacon +bacteria +bacterium +badass +badge +badland +badly +badness +baffle +baffling +bagel +bagful +baggage +bagged +baggie +bagginess +bagging +baggy +bagpipe +baguette +baked +bakery +bakeshop +baking +balance +balancing +balcony +balmy +balsamic +bamboo +banana +banish +banister +banjo +bankable +bankbook +banked +banker +banking +banknote +bankroll +banner +bannister +banshee +banter +barbecue +barbed +barbell +barber +barcode +barge +bargraph +barista +baritone +barley +barmaid +barman +barn +barometer +barrack +barracuda +barrel +barrette +barricade +barrier +barstool +bartender +barterer +bash +basically +basics +basil +basin +basis +basket +batboy +batch +bath +baton +bats +battalion +battered +battering +battery +batting +battle +bauble +bazooka +blabber +bladder +blade +blah +blame +blaming +blanching +blandness +blank +blaspheme +blasphemy +blast +blatancy +blatantly +blazer +blazing +bleach +bleak +bleep +blemish +blend +bless +blighted +blimp +bling +blinked +blinker +blinking +blinks +blip +blissful +blitz +blizzard +bloated +bloating +blob +blog +bloomers +blooming +blooper +blot +blouse +blubber +bluff +bluish +blunderer +blunt +blurb +blurred +blurry +blurt +blush +blustery +boaster +boastful +boasting +boat +bobbed +bobbing +bobble +bobcat +bobsled +bobtail +bodacious +body +bogged +boggle +bogus +boil +bok +bolster +bolt +bonanza +bonded +bonding +bondless +boned +bonehead +boneless +bonelike +boney +bonfire +bonnet +bonsai +bonus +bony +boogeyman +boogieman +book +boondocks +booted +booth +bootie +booting +bootlace +bootleg +boots +boozy +borax +boring +borough +borrower +borrowing +boss +botanical +botanist +botany +botch +both +bottle +bottling +bottom +bounce +bouncing +bouncy +bounding +boundless +bountiful +bovine +boxcar +boxer +boxing +boxlike +boxy +breach +breath +breeches +breeching +breeder +breeding +breeze +breezy +brethren +brewery +brewing +briar +bribe +brick +bride +bridged +brigade +bright +brilliant +brim +bring +brink +brisket +briskly +briskness +bristle +brittle +broadband +broadcast +broaden +broadly +broadness +broadside +broadways +broiler +broiling +broken +broker +bronchial +bronco +bronze +bronzing +brook +broom +brought +browbeat +brownnose +browse +browsing +bruising +brunch +brunette +brunt +brush +brussels +brute +brutishly +bubble +bubbling +bubbly +buccaneer +bucked +bucket +buckle +buckshot +buckskin +bucktooth +buckwheat +buddhism +buddhist +budding +buddy +budget +buffalo +buffed +buffer +buffing +buffoon +buggy +bulb +bulge +bulginess +bulgur +bulk +bulldog +bulldozer +bullfight +bullfrog +bullhorn +bullion +bullish +bullpen +bullring +bullseye +bullwhip +bully +bunch +bundle +bungee +bunion +bunkbed +bunkhouse +bunkmate +bunny +bunt +busboy +bush +busily +busload +bust +busybody +buzz +cabana +cabbage +cabbie +cabdriver +cable +caboose +cache +cackle +cacti +cactus +caddie +caddy +cadet +cadillac +cadmium +cage +cahoots +cake +calamari +calamity +calcium +calculate +calculus +caliber +calibrate +calm +caloric +calorie +calzone +camcorder +cameo +camera +camisole +camper +campfire +camping +campsite +campus +canal +canary +cancel +candied +candle +candy +cane +canine +canister +cannabis +canned +canning +cannon +cannot +canola +canon +canopener +canopy +canteen +canyon +capable +capably +capacity +cape +capillary +capital +capitol +capped +capricorn +capsize +capsule +caption +captivate +captive +captivity +capture +caramel +carat +caravan +carbon +cardboard +carded +cardiac +cardigan +cardinal +cardstock +carefully +caregiver +careless +caress +caretaker +cargo +caring +carless +carload +carmaker +carnage +carnation +carnival +carnivore +carol +carpenter +carpentry +carpool +carport +carried +carrot +carrousel +carry +cartel +cartload +carton +cartoon +cartridge +cartwheel +carve +carving +carwash +cascade +case +cash +casing +casino +casket +cassette +casually +casualty +catacomb +catalog +catalyst +catalyze +catapult +cataract +catatonic +catcall +catchable +catcher +catching +catchy +caterer +catering +catfight +catfish +cathedral +cathouse +catlike +catnap +catnip +catsup +cattail +cattishly +cattle +catty +catwalk +caucasian +caucus +causal +causation +cause +causing +cauterize +caution +cautious +cavalier +cavalry +caviar +cavity +cedar +celery +celestial +celibacy +celibate +celtic +cement +census +ceramics +ceremony +certainly +certainty +certified +certify +cesarean +cesspool +chafe +chaffing +chain +chair +chalice +challenge +chamber +chamomile +champion +chance +change +channel +chant +chaos +chaperone +chaplain +chapped +chaps +chapter +character +charbroil +charcoal +charger +charging +chariot +charity +charm +charred +charter +charting +chase +chasing +chaste +chastise +chastity +chatroom +chatter +chatting +chatty +cheating +cheddar +cheek +cheer +cheese +cheesy +chef +chemicals +chemist +chemo +cherisher +cherub +chess +chest +chevron +chevy +chewable +chewer +chewing +chewy +chief +chihuahua +childcare +childhood +childish +childless +childlike +chili +chill +chimp +chip +chirping +chirpy +chitchat +chivalry +chive +chloride +chlorine +choice +chokehold +choking +chomp +chooser +choosing +choosy +chop +chosen +chowder +chowtime +chrome +chubby +chuck +chug +chummy +chump +chunk +churn +chute +cider +cilantro +cinch +cinema +cinnamon +circle +circling +circular +circulate +circus +citable +citadel +citation +citizen +citric +citrus +city +civic +civil +clad +claim +clambake +clammy +clamor +clamp +clamshell +clang +clanking +clapped +clapper +clapping +clarify +clarinet +clarity +clash +clasp +class +clatter +clause +clavicle +claw +clay +clean +clear +cleat +cleaver +cleft +clench +clergyman +clerical +clerk +clever +clicker +client +climate +climatic +cling +clinic +clinking +clip +clique +cloak +clobber +clock +clone +cloning +closable +closure +clothes +clothing +cloud +clover +clubbed +clubbing +clubhouse +clump +clumsily +clumsy +clunky +clustered +clutch +clutter +coach +coagulant +coastal +coaster +coasting +coastland +coastline +coat +coauthor +cobalt +cobbler +cobweb +cocoa +coconut +cod +coeditor +coerce +coexist +coffee +cofounder +cognition +cognitive +cogwheel +coherence +coherent +cohesive +coil +coke +cola +cold +coleslaw +coliseum +collage +collapse +collar +collected +collector +collide +collie +collision +colonial +colonist +colonize +colony +colossal +colt +coma +come +comfort +comfy +comic +coming +comma +commence +commend +comment +commerce +commode +commodity +commodore +common +commotion +commute +commuting +compacted +compacter +compactly +compactor +companion +company +compare +compel +compile +comply +component +composed +composer +composite +compost +composure +compound +compress +comprised +computer +computing +comrade +concave +conceal +conceded +concept +concerned +concert +conch +concierge +concise +conclude +concrete +concur +condense +condiment +condition +condone +conducive +conductor +conduit +cone +confess +confetti +confidant +confident +confider +confiding +configure +confined +confining +confirm +conflict +conform +confound +confront +confused +confusing +confusion +congenial +congested +congrats +congress +conical +conjoined +conjure +conjuror +connected +connector +consensus +consent +console +consoling +consonant +constable +constant +constrain +constrict +construct +consult +consumer +consuming +contact +container +contempt +contend +contented +contently +contents +contest +context +contort +contour +contrite +control +contusion +convene +convent +copartner +cope +copied +copier +copilot +coping +copious +copper +copy +coral +cork +cornball +cornbread +corncob +cornea +corned +corner +cornfield +cornflake +cornhusk +cornmeal +cornstalk +corny +coronary +coroner +corporal +corporate +corral +correct +corridor +corrode +corroding +corrosive +corsage +corset +cortex +cosigner +cosmetics +cosmic +cosmos +cosponsor +cost +cottage +cotton +couch +cough +could +countable +countdown +counting +countless +country +county +courier +covenant +cover +coveted +coveting +coyness +cozily +coziness +cozy +crabbing +crabgrass +crablike +crabmeat +cradle +cradling +crafter +craftily +craftsman +craftwork +crafty +cramp +cranberry +crane +cranial +cranium +crank +crate +crave +craving +crawfish +crawlers +crawling +crayfish +crayon +crazed +crazily +craziness +crazy +creamed +creamer +creamlike +crease +creasing +creatable +create +creation +creative +creature +credible +credibly +credit +creed +creme +creole +crepe +crept +crescent +crested +cresting +crestless +crevice +crewless +crewman +crewmate +crib +cricket +cried +crier +crimp +crimson +cringe +cringing +crinkle +crinkly +crisped +crisping +crisply +crispness +crispy +criteria +critter +croak +crock +crook +croon +crop +cross +crouch +crouton +crowbar +crowd +crown +crucial +crudely +crudeness +cruelly +cruelness +cruelty +crumb +crummiest +crummy +crumpet +crumpled +cruncher +crunching +crunchy +crusader +crushable +crushed +crusher +crushing +crust +crux +crying +cryptic +crystal +cubbyhole +cube +cubical +cubicle +cucumber +cuddle +cuddly +cufflink +culinary +culminate +culpable +culprit +cultivate +cultural +culture +cupbearer +cupcake +cupid +cupped +cupping +curable +curator +curdle +cure +curfew +curing +curled +curler +curliness +curling +curly +curry +curse +cursive +cursor +curtain +curtly +curtsy +curvature +curve +curvy +cushy +cusp +cussed +custard +custodian +custody +customary +customer +customize +customs +cut +cycle +cyclic +cycling +cyclist +cylinder +cymbal +cytoplasm +cytoplast +dab +dad +daffodil +dagger +daily +daintily +dainty +dairy +daisy +dallying +dance +dancing +dandelion +dander +dandruff +dandy +danger +dangle +dangling +daredevil +dares +daringly +darkened +darkening +darkish +darkness +darkroom +darling +darn +dart +darwinism +dash +dastardly +data +datebook +dating +daughter +daunting +dawdler +dawn +daybed +daybreak +daycare +daydream +daylight +daylong +dayroom +daytime +dazzler +dazzling +deacon +deafening +deafness +dealer +dealing +dealmaker +dealt +dean +debatable +debate +debating +debit +debrief +debtless +debtor +debug +debunk +decade +decaf +decal +decathlon +decay +deceased +deceit +deceiver +deceiving +december +decency +decent +deception +deceptive +decibel +decidable +decimal +decimeter +decipher +deck +declared +decline +decode +decompose +decorated +decorator +decoy +decrease +decree +dedicate +dedicator +deduce +deduct +deed +deem +deepen +deeply +deepness +deface +defacing +defame +default +defeat +defection +defective +defendant +defender +defense +defensive +deferral +deferred +defiance +defiant +defile +defiling +define +definite +deflate +deflation +deflator +deflected +deflector +defog +deforest +defraud +defrost +deftly +defuse +defy +degraded +degrading +degrease +degree +dehydrate +deity +dejected +delay +delegate +delegator +delete +deletion +delicacy +delicate +delicious +delighted +delirious +delirium +deliverer +delivery +delouse +delta +deluge +delusion +deluxe +demanding +demeaning +demeanor +demise +democracy +democrat +demote +demotion +demystify +denatured +deniable +denial +denim +denote +dense +density +dental +dentist +denture +deny +deodorant +deodorize +departed +departure +depict +deplete +depletion +deplored +deploy +deport +depose +depraved +depravity +deprecate +depress +deprive +depth +deputize +deputy +derail +deranged +derby +derived +desecrate +deserve +deserving +designate +designed +designer +designing +deskbound +desktop +deskwork +desolate +despair +despise +despite +destiny +destitute +destruct +detached +detail +detection +detective +detector +detention +detergent +detest +detonate +detonator +detoxify +detract +deuce +devalue +deviancy +deviant +deviate +deviation +deviator +device +devious +devotedly +devotee +devotion +devourer +devouring +devoutly +dexterity +dexterous +diabetes +diabetic +diabolic +diagnoses +diagnosis +diagram +dial +diameter +diaper +diaphragm +diary +dice +dicing +dictate +dictation +dictator +difficult +diffused +diffuser +diffusion +diffusive +dig +dilation +diligence +diligent +dill +dilute +dime +diminish +dimly +dimmed +dimmer +dimness +dimple +diner +dingbat +dinghy +dinginess +dingo +dingy +dining +dinner +diocese +dioxide +diploma +dipped +dipper +dipping +directed +direction +directive +directly +directory +direness +dirtiness +disabled +disagree +disallow +disarm +disarray +disaster +disband +disbelief +disburse +discard +discern +discharge +disclose +discolor +discount +discourse +discover +discuss +disdain +disengage +disfigure +disgrace +dish +disinfect +disjoin +disk +dislike +disliking +dislocate +dislodge +disloyal +dismantle +dismay +dismiss +dismount +disobey +disorder +disown +disparate +disparity +dispatch +dispense +dispersal +dispersed +disperser +displace +display +displease +disposal +dispose +disprove +dispute +disregard +disrupt +dissuade +distance +distant +distaste +distill +distinct +distort +distract +distress +district +distrust +ditch +ditto +ditzy +dividable +divided +dividend +dividers +dividing +divinely +diving +divinity +divisible +divisibly +division +divisive +divorcee +dizziness +dizzy +doable +docile +dock +doctrine +document +dodge +dodgy +doily +doing +dole +dollar +dollhouse +dollop +dolly +dolphin +domain +domelike +domestic +dominion +dominoes +donated +donation +donator +donor +donut +doodle +doorbell +doorframe +doorknob +doorman +doormat +doornail +doorpost +doorstep +doorstop +doorway +doozy +dork +dormitory +dorsal +dosage +dose +dotted +doubling +douche +dove +down +dowry +doze +drab +dragging +dragonfly +dragonish +dragster +drainable +drainage +drained +drainer +drainpipe +dramatic +dramatize +drank +drapery +drastic +draw +dreaded +dreadful +dreadlock +dreamboat +dreamily +dreamland +dreamless +dreamlike +dreamt +dreamy +drearily +dreary +drench +dress +drew +dribble +dried +drier +drift +driller +drilling +drinkable +drinking +dripping +drippy +drivable +driven +driver +driveway +driving +drizzle +drizzly +drone +drool +droop +drop-down +dropbox +dropkick +droplet +dropout +dropper +drove +drown +drowsily +drudge +drum +dry +dubbed +dubiously +duchess +duckbill +ducking +duckling +ducktail +ducky +duct +dude +duffel +dugout +duh +duke +duller +dullness +duly +dumping +dumpling +dumpster +duo +dupe +duplex +duplicate +duplicity +durable +durably +duration +duress +during +dusk +dust +dutiful +duty +duvet +dwarf +dweeb +dwelled +dweller +dwelling +dwindle +dwindling +dynamic +dynamite +dynasty +dyslexia +dyslexic +each +eagle +earache +eardrum +earflap +earful +earlobe +early +earmark +earmuff +earphone +earpiece +earplugs +earring +earshot +earthen +earthlike +earthling +earthly +earthworm +earthy +earwig +easeful +easel +easiest +easily +easiness +easing +eastbound +eastcoast +easter +eastward +eatable +eaten +eatery +eating +eats +ebay +ebony +ebook +ecard +eccentric +echo +eclair +eclipse +ecologist +ecology +economic +economist +economy +ecosphere +ecosystem +edge +edginess +edging +edgy +edition +editor +educated +education +educator +eel +effective +effects +efficient +effort +eggbeater +egging +eggnog +eggplant +eggshell +egomaniac +egotism +egotistic +either +eject +elaborate +elastic +elated +elbow +eldercare +elderly +eldest +electable +election +elective +elephant +elevate +elevating +elevation +elevator +eleven +elf +eligible +eligibly +eliminate +elite +elitism +elixir +elk +ellipse +elliptic +elm +elongated +elope +eloquence +eloquent +elsewhere +elude +elusive +elves +email +embargo +embark +embassy +embattled +embellish +ember +embezzle +emblaze +emblem +embody +embolism +emboss +embroider +emcee +emerald +emergency +emission +emit +emote +emoticon +emotion +empathic +empathy +emperor +emphases +emphasis +emphasize +emphatic +empirical +employed +employee +employer +emporium +empower +emptier +emptiness +empty +emu +enable +enactment +enamel +enchanted +enchilada +encircle +enclose +enclosure +encode +encore +encounter +encourage +encroach +encrust +encrypt +endanger +endeared +endearing +ended +ending +endless +endnote +endocrine +endorphin +endorse +endowment +endpoint +endurable +endurance +enduring +energetic +energize +energy +enforced +enforcer +engaged +engaging +engine +engorge +engraved +engraver +engraving +engross +engulf +enhance +enigmatic +enjoyable +enjoyably +enjoyer +enjoying +enjoyment +enlarged +enlarging +enlighten +enlisted +enquirer +enrage +enrich +enroll +enslave +ensnare +ensure +entail +entangled +entering +entertain +enticing +entire +entitle +entity +entomb +entourage +entrap +entree +entrench +entrust +entryway +entwine +enunciate +envelope +enviable +enviably +envious +envision +envoy +envy +enzyme +epic +epidemic +epidermal +epidermis +epidural +epilepsy +epileptic +epilogue +epiphany +episode +equal +equate +equation +equator +equinox +equipment +equity +equivocal +eradicate +erasable +erased +eraser +erasure +ergonomic +errand +errant +erratic +error +erupt +escalate +escalator +escapable +escapade +escapist +escargot +eskimo +esophagus +espionage +espresso +esquire +essay +essence +essential +establish +estate +esteemed +estimate +estimator +estranged +estrogen +etching +eternal +eternity +ethanol +ether +ethically +ethics +euphemism +evacuate +evacuee +evade +evaluate +evaluator +evaporate +evasion +evasive +even +everglade +evergreen +everybody +everyday +everyone +evict +evidence +evident +evil +evoke +evolution +evolve +exact +exalted +example +excavate +excavator +exceeding +exception +excess +exchange +excitable +exciting +exclaim +exclude +excluding +exclusion +exclusive +excretion +excretory +excursion +excusable +excusably +excuse +exemplary +exemplify +exemption +exerciser +exert +exes +exfoliate +exhale +exhaust +exhume +exile +existing +exit +exodus +exonerate +exorcism +exorcist +expand +expanse +expansion +expansive +expectant +expedited +expediter +expel +expend +expenses +expensive +expert +expire +expiring +explain +expletive +explicit +explode +exploit +explore +exploring +exponent +exporter +exposable +expose +exposure +express +expulsion +exquisite +extended +extending +extent +extenuate +exterior +external +extinct +extortion +extradite +extras +extrovert +extrude +extruding +exuberant +fable +fabric +fabulous +facebook +facecloth +facedown +faceless +facelift +faceplate +faceted +facial +facility +facing +facsimile +faction +factoid +factor +factsheet +factual +faculty +fade +fading +failing +falcon +fall +false +falsify +fame +familiar +family +famine +famished +fanatic +fancied +fanciness +fancy +fanfare +fang +fanning +fantasize +fantastic +fantasy +fascism +fastball +faster +fasting +fastness +faucet +favorable +favorably +favored +favoring +favorite +fax +feast +federal +fedora +feeble +feed +feel +feisty +feline +felt-tip +feminine +feminism +feminist +feminize +femur +fence +fencing +fender +ferment +fernlike +ferocious +ferocity +ferret +ferris +ferry +fervor +fester +festival +festive +festivity +fetal +fetch +fever +fiber +fiction +fiddle +fiddling +fidelity +fidgeting +fidgety +fifteen +fifth +fiftieth +fifty +figment +figure +figurine +filing +filled +filler +filling +film +filter +filth +filtrate +finale +finalist +finalize +finally +finance +financial +finch +fineness +finer +finicky +finished +finisher +finishing +finite +finless +finlike +fiscally +fit +five +flaccid +flagman +flagpole +flagship +flagstick +flagstone +flail +flakily +flaky +flame +flammable +flanked +flanking +flannels +flap +flaring +flashback +flashbulb +flashcard +flashily +flashing +flashy +flask +flatbed +flatfoot +flatly +flatness +flatten +flattered +flatterer +flattery +flattop +flatware +flatworm +flavored +flavorful +flavoring +flaxseed +fled +fleshed +fleshy +flick +flier +flight +flinch +fling +flint +flip +flirt +float +flock +flogging +flop +floral +florist +floss +flounder +flyable +flyaway +flyer +flying +flyover +flypaper +foam +foe +fog +foil +folic +folk +follicle +follow +fondling +fondly +fondness +fondue +font +food +fool +footage +football +footbath +footboard +footer +footgear +foothill +foothold +footing +footless +footman +footnote +footpad +footpath +footprint +footrest +footsie +footsore +footwear +footwork +fossil +foster +founder +founding +fountain +fox +foyer +fraction +fracture +fragile +fragility +fragment +fragrance +fragrant +frail +frame +framing +frantic +fraternal +frayed +fraying +frays +freckled +freckles +freebase +freebee +freebie +freedom +freefall +freehand +freeing +freeload +freely +freemason +freeness +freestyle +freeware +freeway +freewill +freezable +freezing +freight +french +frenzied +frenzy +frequency +frequent +fresh +fretful +fretted +friction +friday +fridge +fried +friend +frighten +frightful +frigidity +frigidly +frill +fringe +frisbee +frisk +fritter +frivolous +frolic +from +front +frostbite +frosted +frostily +frosting +frostlike +frosty +froth +frown +frozen +fructose +frugality +frugally +fruit +frustrate +frying +gab +gaffe +gag +gainfully +gaining +gains +gala +gallantly +galleria +gallery +galley +gallon +gallows +gallstone +galore +galvanize +gambling +game +gaming +gamma +gander +gangly +gangrene +gangway +gap +garage +garbage +garden +gargle +garland +garlic +garment +garnet +garnish +garter +gas +gatherer +gathering +gating +gauging +gauntlet +gauze +gave +gawk +gazing +gear +gecko +geek +geiger +gem +gender +generic +generous +genetics +genre +gentile +gentleman +gently +gents +geography +geologic +geologist +geology +geometric +geometry +geranium +gerbil +geriatric +germicide +germinate +germless +germproof +gestate +gestation +gesture +getaway +getting +getup +giant +gibberish +giblet +giddily +giddiness +giddy +gift +gigabyte +gigahertz +gigantic +giggle +giggling +giggly +gigolo +gilled +gills +gimmick +girdle +giveaway +given +giver +giving +gizmo +gizzard +glacial +glacier +glade +gladiator +gladly +glamorous +glamour +glance +glancing +glandular +glare +glaring +glass +glaucoma +glazing +gleaming +gleeful +glider +gliding +glimmer +glimpse +glisten +glitch +glitter +glitzy +gloater +gloating +gloomily +gloomy +glorified +glorifier +glorify +glorious +glory +gloss +glove +glowing +glowworm +glucose +glue +gluten +glutinous +glutton +gnarly +gnat +goal +goatskin +goes +goggles +going +goldfish +goldmine +goldsmith +golf +goliath +gonad +gondola +gone +gong +good +gooey +goofball +goofiness +goofy +google +goon +gopher +gore +gorged +gorgeous +gory +gosling +gossip +gothic +gotten +gout +gown +grab +graceful +graceless +gracious +gradation +graded +grader +gradient +grading +gradually +graduate +graffiti +grafted +grafting +grain +granddad +grandkid +grandly +grandma +grandpa +grandson +granite +granny +granola +grant +granular +grape +graph +grapple +grappling +grasp +grass +gratified +gratify +grating +gratitude +gratuity +gravel +graveness +graves +graveyard +gravitate +gravity +gravy +gray +grazing +greasily +greedily +greedless +greedy +green +greeter +greeting +grew +greyhound +grid +grief +grievance +grieving +grievous +grill +grimace +grimacing +grime +griminess +grimy +grinch +grinning +grip +gristle +grit +groggily +groggy +groin +groom +groove +grooving +groovy +grope +ground +grouped +grout +grove +grower +growing +growl +grub +grudge +grudging +grueling +gruffly +grumble +grumbling +grumbly +grumpily +grunge +grunt +guacamole +guidable +guidance +guide +guiding +guileless +guise +gulf +gullible +gully +gulp +gumball +gumdrop +gumminess +gumming +gummy +gurgle +gurgling +guru +gush +gusto +gusty +gutless +guts +gutter +guy +guzzler +gyration +habitable +habitant +habitat +habitual +hacked +hacker +hacking +hacksaw +had +haggler +haiku +half +halogen +halt +halved +halves +hamburger +hamlet +hammock +hamper +hamster +hamstring +handbag +handball +handbook +handbrake +handcart +handclap +handclasp +handcraft +handcuff +handed +handful +handgrip +handgun +handheld +handiness +handiwork +handlebar +handled +handler +handling +handmade +handoff +handpick +handprint +handrail +handsaw +handset +handsfree +handshake +handstand +handwash +handwork +handwoven +handwrite +handyman +hangnail +hangout +hangover +hangup +hankering +hankie +hanky +haphazard +happening +happier +happiest +happily +happiness +happy +harbor +hardcopy +hardcore +hardcover +harddisk +hardened +hardener +hardening +hardhat +hardhead +hardiness +hardly +hardness +hardship +hardware +hardwired +hardwood +hardy +harmful +harmless +harmonica +harmonics +harmonize +harmony +harness +harpist +harsh +harvest +hash +hassle +haste +hastily +hastiness +hasty +hatbox +hatchback +hatchery +hatchet +hatching +hatchling +hate +hatless +hatred +haunt +haven +hazard +hazelnut +hazily +haziness +hazing +hazy +headache +headband +headboard +headcount +headdress +headed +header +headfirst +headgear +heading +headlamp +headless +headlock +headphone +headpiece +headrest +headroom +headscarf +headset +headsman +headstand +headstone +headway +headwear +heap +heat +heave +heavily +heaviness +heaving +hedge +hedging +heftiness +hefty +helium +helmet +helper +helpful +helping +helpless +helpline +hemlock +hemstitch +hence +henchman +henna +herald +herbal +herbicide +herbs +heritage +hermit +heroics +heroism +herring +herself +hertz +hesitancy +hesitant +hesitate +hexagon +hexagram +hubcap +huddle +huddling +huff +hug +hula +hulk +hull +human +humble +humbling +humbly +humid +humiliate +humility +humming +hummus +humongous +humorist +humorless +humorous +humpback +humped +humvee +hunchback +hundredth +hunger +hungrily +hungry +hunk +hunter +hunting +huntress +huntsman +hurdle +hurled +hurler +hurling +hurray +hurricane +hurried +hurry +hurt +husband +hush +husked +huskiness +hut +hybrid +hydrant +hydrated +hydration +hydrogen +hydroxide +hyperlink +hypertext +hyphen +hypnoses +hypnosis +hypnotic +hypnotism +hypnotist +hypnotize +hypocrisy +hypocrite +ibuprofen +ice +iciness +icing +icky +icon +icy +idealism +idealist +idealize +ideally +idealness +identical +identify +identity +ideology +idiocy +idiom +idly +igloo +ignition +ignore +iguana +illicitly +illusion +illusive +image +imaginary +imagines +imaging +imbecile +imitate +imitation +immature +immerse +immersion +imminent +immobile +immodest +immorally +immortal +immovable +immovably +immunity +immunize +impaired +impale +impart +impatient +impeach +impeding +impending +imperfect +imperial +impish +implant +implement +implicate +implicit +implode +implosion +implosive +imply +impolite +important +importer +impose +imposing +impotence +impotency +impotent +impound +imprecise +imprint +imprison +impromptu +improper +improve +improving +improvise +imprudent +impulse +impulsive +impure +impurity +iodine +iodize +ion +ipad +iphone +ipod +irate +irk +iron +irregular +irrigate +irritable +irritably +irritant +irritate +islamic +islamist +isolated +isolating +isolation +isotope +issue +issuing +italicize +italics +item +itinerary +itunes +ivory +ivy +jab +jackal +jacket +jackknife +jackpot +jailbird +jailbreak +jailer +jailhouse +jalapeno +jam +janitor +january +jargon +jarring +jasmine +jaundice +jaunt +java +jawed +jawless +jawline +jaws +jaybird +jaywalker +jazz +jeep +jeeringly +jellied +jelly +jersey +jester +jet +jiffy +jigsaw +jimmy +jingle +jingling +jinx +jitters +jittery +job +jockey +jockstrap +jogger +jogging +john +joining +jokester +jokingly +jolliness +jolly +jolt +jot +jovial +joyfully +joylessly +joyous +joyride +joystick +jubilance +jubilant +judge +judgingly +judicial +judiciary +judo +juggle +juggling +jugular +juice +juiciness +juicy +jujitsu +jukebox +july +jumble +jumbo +jump +junction +juncture +june +junior +juniper +junkie +junkman +junkyard +jurist +juror +jury +justice +justifier +justify +justly +justness +juvenile +kabob +kangaroo +karaoke +karate +karma +kebab +keenly +keenness +keep +keg +kelp +kennel +kept +kerchief +kerosene +kettle +kick +kiln +kilobyte +kilogram +kilometer +kilowatt +kilt +kimono +kindle +kindling +kindly +kindness +kindred +kinetic +kinfolk +king +kinship +kinsman +kinswoman +kissable +kisser +kissing +kitchen +kite +kitten +kitty +kiwi +kleenex +knapsack +knee +knelt +knickers +knoll +koala +kooky +kosher +krypton +kudos +kung +labored +laborer +laboring +laborious +labrador +ladder +ladies +ladle +ladybug +ladylike +lagged +lagging +lagoon +lair +lake +lance +landed +landfall +landfill +landing +landlady +landless +landline +landlord +landmark +landmass +landmine +landowner +landscape +landside +landslide +language +lankiness +lanky +lantern +lapdog +lapel +lapped +lapping +laptop +lard +large +lark +lash +lasso +last +latch +late +lather +latitude +latrine +latter +latticed +launch +launder +laundry +laurel +lavender +lavish +laxative +lazily +laziness +lazy +lecturer +left +legacy +legal +legend +legged +leggings +legible +legibly +legislate +lego +legroom +legume +legwarmer +legwork +lemon +lend +length +lens +lent +leotard +lesser +letdown +lethargic +lethargy +letter +lettuce +level +leverage +levers +levitate +levitator +liability +liable +liberty +librarian +library +licking +licorice +lid +life +lifter +lifting +liftoff +ligament +likely +likeness +likewise +liking +lilac +lilly +lily +limb +limeade +limelight +limes +limit +limping +limpness +line +lingo +linguini +linguist +lining +linked +linoleum +linseed +lint +lion +lip +liquefy +liqueur +liquid +lisp +list +litigate +litigator +litmus +litter +little +livable +lived +lively +liver +livestock +lividly +living +lizard +lubricant +lubricate +lucid +luckily +luckiness +luckless +lucrative +ludicrous +lugged +lukewarm +lullaby +lumber +luminance +luminous +lumpiness +lumping +lumpish +lunacy +lunar +lunchbox +luncheon +lunchroom +lunchtime +lung +lurch +lure +luridness +lurk +lushly +lushness +luster +lustfully +lustily +lustiness +lustrous +lusty +luxurious +luxury +lying +lyrically +lyricism +lyricist +lyrics +macarena +macaroni +macaw +mace +machine +machinist +magazine +magenta +maggot +magical +magician +magma +magnesium +magnetic +magnetism +magnetize +magnifier +magnify +magnitude +magnolia +mahogany +maimed +majestic +majesty +majorette +majority +makeover +maker +makeshift +making +malformed +malt +mama +mammal +mammary +mammogram +manager +managing +manatee +mandarin +mandate +mandatory +mandolin +manger +mangle +mango +mangy +manhandle +manhole +manhood +manhunt +manicotti +manicure +manifesto +manila +mankind +manlike +manliness +manly +manmade +manned +mannish +manor +manpower +mantis +mantra +manual +many +map +marathon +marauding +marbled +marbles +marbling +march +mardi +margarine +margarita +margin +marigold +marina +marine +marital +maritime +marlin +marmalade +maroon +married +marrow +marry +marshland +marshy +marsupial +marvelous +marxism +mascot +masculine +mashed +mashing +massager +masses +massive +mastiff +matador +matchbook +matchbox +matcher +matching +matchless +material +maternal +maternity +math +mating +matriarch +matrimony +matrix +matron +matted +matter +maturely +maturing +maturity +mauve +maverick +maximize +maximum +maybe +mayday +mayflower +moaner +moaning +mobile +mobility +mobilize +mobster +mocha +mocker +mockup +modified +modify +modular +modulator +module +moisten +moistness +moisture +molar +molasses +mold +molecular +molecule +molehill +mollusk +mom +monastery +monday +monetary +monetize +moneybags +moneyless +moneywise +mongoose +mongrel +monitor +monkhood +monogamy +monogram +monologue +monopoly +monorail +monotone +monotype +monoxide +monsieur +monsoon +monstrous +monthly +monument +moocher +moodiness +moody +mooing +moonbeam +mooned +moonlight +moonlike +moonlit +moonrise +moonscape +moonshine +moonstone +moonwalk +mop +morale +morality +morally +morbidity +morbidly +morphine +morphing +morse +mortality +mortally +mortician +mortified +mortify +mortuary +mosaic +mossy +most +mothball +mothproof +motion +motivate +motivator +motive +motocross +motor +motto +mountable +mountain +mounted +mounting +mourner +mournful +mouse +mousiness +moustache +mousy +mouth +movable +move +movie +moving +mower +mowing +much +muck +mud +mug +mulberry +mulch +mule +mulled +mullets +multiple +multiply +multitask +multitude +mumble +mumbling +mumbo +mummified +mummify +mummy +mumps +munchkin +mundane +municipal +muppet +mural +murkiness +murky +murmuring +muscular +museum +mushily +mushiness +mushroom +mushy +music +musket +muskiness +musky +mustang +mustard +muster +mustiness +musty +mutable +mutate +mutation +mute +mutilated +mutilator +mutiny +mutt +mutual +muzzle +myself +myspace +mystified +mystify +myth +nacho +nag +nail +name +naming +nanny +nanometer +nape +napkin +napped +napping +nappy +narrow +nastily +nastiness +national +native +nativity +natural +nature +naturist +nautical +navigate +navigator +navy +nearby +nearest +nearly +nearness +neatly +neatness +nebula +nebulizer +nectar +negate +negation +negative +neglector +negligee +negligent +negotiate +nemeses +nemesis +neon +nephew +nerd +nervous +nervy +nest +net +neurology +neuron +neurosis +neurotic +neuter +neutron +never +next +nibble +nickname +nicotine +niece +nifty +nimble +nimbly +nineteen +ninetieth +ninja +nintendo +ninth +nuclear +nuclei +nucleus +nugget +nullify +number +numbing +numbly +numbness +numeral +numerate +numerator +numeric +numerous +nuptials +nursery +nursing +nurture +nutcase +nutlike +nutmeg +nutrient +nutshell +nuttiness +nutty +nuzzle +nylon +oaf +oak +oasis +oat +obedience +obedient +obituary +object +obligate +obliged +oblivion +oblivious +oblong +obnoxious +oboe +obscure +obscurity +observant +observer +observing +obsessed +obsession +obsessive +obsolete +obstacle +obstinate +obstruct +obtain +obtrusive +obtuse +obvious +occultist +occupancy +occupant +occupier +occupy +ocean +ocelot +octagon +octane +october +octopus +ogle +oil +oink +ointment +okay +old +olive +olympics +omega +omen +ominous +omission +omit +omnivore +onboard +oncoming +ongoing +onion +online +onlooker +only +onscreen +onset +onshore +onslaught +onstage +onto +onward +onyx +oops +ooze +oozy +opacity +opal +open +operable +operate +operating +operation +operative +operator +opium +opossum +opponent +oppose +opposing +opposite +oppressed +oppressor +opt +opulently +osmosis +other +otter +ouch +ought +ounce +outage +outback +outbid +outboard +outbound +outbreak +outburst +outcast +outclass +outcome +outdated +outdoors +outer +outfield +outfit +outflank +outgoing +outgrow +outhouse +outing +outlast +outlet +outline +outlook +outlying +outmatch +outmost +outnumber +outplayed +outpost +outpour +output +outrage +outrank +outreach +outright +outscore +outsell +outshine +outshoot +outsider +outskirts +outsmart +outsource +outspoken +outtakes +outthink +outward +outweigh +outwit +oval +ovary +oven +overact +overall +overarch +overbid +overbill +overbite +overblown +overboard +overbook +overbuilt +overcast +overcoat +overcome +overcook +overcrowd +overdraft +overdrawn +overdress +overdrive +overdue +overeager +overeater +overexert +overfed +overfeed +overfill +overflow +overfull +overgrown +overhand +overhang +overhaul +overhead +overhear +overheat +overhung +overjoyed +overkill +overlabor +overlaid +overlap +overlay +overload +overlook +overlord +overlying +overnight +overpass +overpay +overplant +overplay +overpower +overprice +overrate +overreach +overreact +override +overripe +overrule +overrun +overshoot +overshot +oversight +oversized +oversleep +oversold +overspend +overstate +overstay +overstep +overstock +overstuff +oversweet +overtake +overthrow +overtime +overtly +overtone +overture +overturn +overuse +overvalue +overview +overwrite +owl +oxford +oxidant +oxidation +oxidize +oxidizing +oxygen +oxymoron +oyster +ozone +paced +pacemaker +pacific +pacifier +pacifism +pacifist +pacify +padded +padding +paddle +paddling +padlock +pagan +pager +paging +pajamas +palace +palatable +palm +palpable +palpitate +paltry +pampered +pamperer +pampers +pamphlet +panama +pancake +pancreas +panda +pandemic +pang +panhandle +panic +panning +panorama +panoramic +panther +pantomime +pantry +pants +pantyhose +paparazzi +papaya +paper +paprika +papyrus +parabola +parachute +parade +paradox +paragraph +parakeet +paralegal +paralyses +paralysis +paralyze +paramedic +parameter +paramount +parasail +parasite +parasitic +parcel +parched +parchment +pardon +parish +parka +parking +parkway +parlor +parmesan +parole +parrot +parsley +parsnip +partake +parted +parting +partition +partly +partner +partridge +party +passable +passably +passage +passcode +passenger +passerby +passing +passion +passive +passivism +passover +passport +password +pasta +pasted +pastel +pastime +pastor +pastrami +pasture +pasty +patchwork +patchy +paternal +paternity +path +patience +patient +patio +patriarch +patriot +patrol +patronage +patronize +pauper +pavement +paver +pavestone +pavilion +paving +pawing +payable +payback +paycheck +payday +payee +payer +paying +payment +payphone +payroll +pebble +pebbly +pecan +pectin +peculiar +peddling +pediatric +pedicure +pedigree +pedometer +pegboard +pelican +pellet +pelt +pelvis +penalize +penalty +pencil +pendant +pending +penholder +penknife +pennant +penniless +penny +penpal +pension +pentagon +pentagram +pep +perceive +percent +perch +percolate +perennial +perfected +perfectly +perfume +periscope +perish +perjurer +perjury +perkiness +perky +perm +peroxide +perpetual +perplexed +persecute +persevere +persuaded +persuader +pesky +peso +pessimism +pessimist +pester +pesticide +petal +petite +petition +petri +petroleum +petted +petticoat +pettiness +petty +petunia +phantom +phobia +phoenix +phonebook +phoney +phonics +phoniness +phony +phosphate +photo +phrase +phrasing +placard +placate +placidly +plank +planner +plant +plasma +plaster +plastic +plated +platform +plating +platinum +platonic +platter +platypus +plausible +plausibly +playable +playback +player +playful +playgroup +playhouse +playing +playlist +playmaker +playmate +playoff +playpen +playroom +playset +plaything +playtime +plaza +pleading +pleat +pledge +plentiful +plenty +plethora +plexiglas +pliable +plod +plop +plot +plow +ploy +pluck +plug +plunder +plunging +plural +plus +plutonium +plywood +poach +pod +poem +poet +pogo +pointed +pointer +pointing +pointless +pointy +poise +poison +poker +poking +polar +police +policy +polio +polish +politely +polka +polo +polyester +polygon +polygraph +polymer +poncho +pond +pony +popcorn +pope +poplar +popper +poppy +popsicle +populace +popular +populate +porcupine +pork +porous +porridge +portable +portal +portfolio +porthole +portion +portly +portside +poser +posh +posing +possible +possibly +possum +postage +postal +postbox +postcard +posted +poster +posting +postnasal +posture +postwar +pouch +pounce +pouncing +pound +pouring +pout +powdered +powdering +powdery +power +powwow +pox +praising +prance +prancing +pranker +prankish +prankster +prayer +praying +preacher +preaching +preachy +preamble +precinct +precise +precision +precook +precut +predator +predefine +predict +preface +prefix +preflight +preformed +pregame +pregnancy +pregnant +preheated +prelaunch +prelaw +prelude +premiere +premises +premium +prenatal +preoccupy +preorder +prepaid +prepay +preplan +preppy +preschool +prescribe +preseason +preset +preshow +president +presoak +press +presume +presuming +preteen +pretended +pretender +pretense +pretext +pretty +pretzel +prevail +prevalent +prevent +preview +previous +prewar +prewashed +prideful +pried +primal +primarily +primary +primate +primer +primp +princess +print +prior +prism +prison +prissy +pristine +privacy +private +privatize +prize +proactive +probable +probably +probation +probe +probing +probiotic +problem +procedure +process +proclaim +procreate +procurer +prodigal +prodigy +produce +product +profane +profanity +professed +professor +profile +profound +profusely +progeny +prognosis +program +progress +projector +prologue +prolonged +promenade +prominent +promoter +promotion +prompter +promptly +prone +prong +pronounce +pronto +proofing +proofread +proofs +propeller +properly +property +proponent +proposal +propose +props +prorate +protector +protegee +proton +prototype +protozoan +protract +protrude +proud +provable +proved +proven +provided +provider +providing +province +proving +provoke +provoking +provolone +prowess +prowler +prowling +proximity +proxy +prozac +prude +prudishly +prune +pruning +pry +psychic +public +publisher +pucker +pueblo +pug +pull +pulmonary +pulp +pulsate +pulse +pulverize +puma +pumice +pummel +punch +punctual +punctuate +punctured +pungent +punisher +punk +pupil +puppet +puppy +purchase +pureblood +purebred +purely +pureness +purgatory +purge +purging +purifier +purify +purist +puritan +purity +purple +purplish +purposely +purr +purse +pursuable +pursuant +pursuit +purveyor +pushcart +pushchair +pusher +pushiness +pushing +pushover +pushpin +pushup +pushy +putdown +putt +puzzle +puzzling +pyramid +pyromania +python +quack +quadrant +quail +quaintly +quake +quaking +qualified +qualifier +qualify +quality +qualm +quantum +quarrel +quarry +quartered +quarterly +quarters +quartet +quench +query +quicken +quickly +quickness +quicksand +quickstep +quiet +quill +quilt +quintet +quintuple +quirk +quit +quiver +quizzical +quotable +quotation +quote +rabid +race +racing +racism +rack +racoon +radar +radial +radiance +radiantly +radiated +radiation +radiator +radio +radish +raffle +raft +rage +ragged +raging +ragweed +raider +railcar +railing +railroad +railway +raisin +rake +raking +rally +ramble +rambling +ramp +ramrod +ranch +rancidity +random +ranged +ranger +ranging +ranked +ranking +ransack +ranting +rants +rare +rarity +rascal +rash +rasping +ravage +raven +ravine +raving +ravioli +ravishing +reabsorb +reach +reacquire +reaction +reactive +reactor +reaffirm +ream +reanalyze +reappear +reapply +reappoint +reapprove +rearrange +rearview +reason +reassign +reassure +reattach +reawake +rebalance +rebate +rebel +rebirth +reboot +reborn +rebound +rebuff +rebuild +rebuilt +reburial +rebuttal +recall +recant +recapture +recast +recede +recent +recess +recharger +recipient +recital +recite +reckless +reclaim +recliner +reclining +recluse +reclusive +recognize +recoil +recollect +recolor +reconcile +reconfirm +reconvene +recopy +record +recount +recoup +recovery +recreate +rectal +rectangle +rectified +rectify +recycled +recycler +recycling +reemerge +reenact +reenter +reentry +reexamine +referable +referee +reference +refill +refinance +refined +refinery +refining +refinish +reflected +reflector +reflex +reflux +refocus +refold +reforest +reformat +reformed +reformer +reformist +refract +refrain +refreeze +refresh +refried +refueling +refund +refurbish +refurnish +refusal +refuse +refusing +refutable +refute +regain +regalia +regally +reggae +regime +region +register +registrar +registry +regress +regretful +regroup +regular +regulate +regulator +rehab +reheat +rehire +rehydrate +reimburse +reissue +reiterate +rejoice +rejoicing +rejoin +rekindle +relapse +relapsing +relatable +related +relation +relative +relax +relay +relearn +release +relenting +reliable +reliably +reliance +reliant +relic +relieve +relieving +relight +relish +relive +reload +relocate +relock +reluctant +rely +remake +remark +remarry +rematch +remedial +remedy +remember +reminder +remindful +remission +remix +remnant +remodeler +remold +remorse +remote +removable +removal +removed +remover +removing +rename +renderer +rendering +rendition +renegade +renewable +renewably +renewal +renewed +renounce +renovate +renovator +rentable +rental +rented +renter +reoccupy +reoccur +reopen +reorder +repackage +repacking +repaint +repair +repave +repaying +repayment +repeal +repeated +repeater +repent +rephrase +replace +replay +replica +reply +reporter +repose +repossess +repost +repressed +reprimand +reprint +reprise +reproach +reprocess +reproduce +reprogram +reps +reptile +reptilian +repugnant +repulsion +repulsive +repurpose +reputable +reputably +request +require +requisite +reroute +rerun +resale +resample +rescuer +reseal +research +reselect +reseller +resemble +resend +resent +reset +reshape +reshoot +reshuffle +residence +residency +resident +residual +residue +resigned +resilient +resistant +resisting +resize +resolute +resolved +resonant +resonate +resort +resource +respect +resubmit +result +resume +resupply +resurface +resurrect +retail +retainer +retaining +retake +retaliate +retention +rethink +retinal +retired +retiree +retiring +retold +retool +retorted +retouch +retrace +retract +retrain +retread +retreat +retrial +retrieval +retriever +retry +return +retying +retype +reunion +reunite +reusable +reuse +reveal +reveler +revenge +revenue +reverb +revered +reverence +reverend +reversal +reverse +reversing +reversion +revert +revisable +revise +revision +revisit +revivable +revival +reviver +reviving +revocable +revoke +revolt +revolver +revolving +reward +rewash +rewind +rewire +reword +rework +rewrap +rewrite +rhyme +ribbon +ribcage +rice +riches +richly +richness +rickety +ricotta +riddance +ridden +ride +riding +rifling +rift +rigging +rigid +rigor +rimless +rimmed +rind +rink +rinse +rinsing +riot +ripcord +ripeness +ripening +ripping +ripple +rippling +riptide +rise +rising +risk +risotto +ritalin +ritzy +rival +riverbank +riverbed +riverboat +riverside +riveter +riveting +roamer +roaming +roast +robbing +robe +robin +robotics +robust +rockband +rocker +rocket +rockfish +rockiness +rocking +rocklike +rockslide +rockstar +rocky +rogue +roman +romp +rope +roping +roster +rosy +rotten +rotting +rotunda +roulette +rounding +roundish +roundness +roundup +roundworm +routine +routing +rover +roving +royal +rubbed +rubber +rubbing +rubble +rubdown +ruby +ruckus +rudder +rug +ruined +rule +rumble +rumbling +rummage +rumor +runaround +rundown +runner +running +runny +runt +runway +rupture +rural +ruse +rush +rust +rut +sabbath +sabotage +sacrament +sacred +sacrifice +sadden +saddlebag +saddled +saddling +sadly +sadness +safari +safeguard +safehouse +safely +safeness +saffron +saga +sage +sagging +saggy +said +saint +sake +salad +salami +salaried +salary +saline +salon +saloon +salsa +salt +salutary +salute +salvage +salvaging +salvation +same +sample +sampling +sanction +sanctity +sanctuary +sandal +sandbag +sandbank +sandbar +sandblast +sandbox +sanded +sandfish +sanding +sandlot +sandpaper +sandpit +sandstone +sandstorm +sandworm +sandy +sanitary +sanitizer +sank +santa +sapling +sappiness +sappy +sarcasm +sarcastic +sardine +sash +sasquatch +sassy +satchel +satiable +satin +satirical +satisfied +satisfy +saturate +saturday +sauciness +saucy +sauna +savage +savanna +saved +savings +savior +savor +saxophone +say +scabbed +scabby +scalded +scalding +scale +scaling +scallion +scallop +scalping +scam +scandal +scanner +scanning +scant +scapegoat +scarce +scarcity +scarecrow +scared +scarf +scarily +scariness +scarring +scary +scavenger +scenic +schedule +schematic +scheme +scheming +schilling +schnapps +scholar +science +scientist +scion +scoff +scolding +scone +scoop +scooter +scope +scorch +scorebook +scorecard +scored +scoreless +scorer +scoring +scorn +scorpion +scotch +scoundrel +scoured +scouring +scouting +scouts +scowling +scrabble +scraggly +scrambled +scrambler +scrap +scratch +scrawny +screen +scribble +scribe +scribing +scrimmage +script +scroll +scrooge +scrounger +scrubbed +scrubber +scruffy +scrunch +scrutiny +scuba +scuff +sculptor +sculpture +scurvy +scuttle +secluded +secluding +seclusion +second +secrecy +secret +sectional +sector +secular +securely +security +sedan +sedate +sedation +sedative +sediment +seduce +seducing +segment +seismic +seizing +seldom +selected +selection +selective +selector +self +seltzer +semantic +semester +semicolon +semifinal +seminar +semisoft +semisweet +senate +senator +send +senior +senorita +sensation +sensitive +sensitize +sensually +sensuous +sepia +september +septic +septum +sequel +sequence +sequester +series +sermon +serotonin +serpent +serrated +serve +service +serving +sesame +sessions +setback +setting +settle +settling +setup +sevenfold +seventeen +seventh +seventy +severity +shabby +shack +shaded +shadily +shadiness +shading +shadow +shady +shaft +shakable +shakily +shakiness +shaking +shaky +shale +shallot +shallow +shame +shampoo +shamrock +shank +shanty +shape +shaping +share +sharpener +sharper +sharpie +sharply +sharpness +shawl +sheath +shed +sheep +sheet +shelf +shell +shelter +shelve +shelving +sherry +shield +shifter +shifting +shiftless +shifty +shimmer +shimmy +shindig +shine +shingle +shininess +shining +shiny +ship +shirt +shivering +shock +shone +shoplift +shopper +shopping +shoptalk +shore +shortage +shortcake +shortcut +shorten +shorter +shorthand +shortlist +shortly +shortness +shorts +shortwave +shorty +shout +shove +showbiz +showcase +showdown +shower +showgirl +showing +showman +shown +showoff +showpiece +showplace +showroom +showy +shrank +shrapnel +shredder +shredding +shrewdly +shriek +shrill +shrimp +shrine +shrink +shrivel +shrouded +shrubbery +shrubs +shrug +shrunk +shucking +shudder +shuffle +shuffling +shun +shush +shut +shy +siamese +siberian +sibling +siding +sierra +siesta +sift +sighing +silenced +silencer +silent +silica +silicon +silk +silliness +silly +silo +silt +silver +similarly +simile +simmering +simple +simplify +simply +sincere +sincerity +singer +singing +single +singular +sinister +sinless +sinner +sinuous +sip +siren +sister +sitcom +sitter +sitting +situated +situation +sixfold +sixteen +sixth +sixties +sixtieth +sixtyfold +sizable +sizably +size +sizing +sizzle +sizzling +skater +skating +skedaddle +skeletal +skeleton +skeptic +sketch +skewed +skewer +skid +skied +skier +skies +skiing +skilled +skillet +skillful +skimmed +skimmer +skimming +skimpily +skincare +skinhead +skinless +skinning +skinny +skintight +skipper +skipping +skirmish +skirt +skittle +skydiver +skylight +skyline +skype +skyrocket +skyward +slab +slacked +slacker +slacking +slackness +slacks +slain +slam +slander +slang +slapping +slapstick +slashed +slashing +slate +slather +slaw +sled +sleek +sleep +sleet +sleeve +slept +sliceable +sliced +slicer +slicing +slick +slider +slideshow +sliding +slighted +slighting +slightly +slimness +slimy +slinging +slingshot +slinky +slip +slit +sliver +slobbery +slogan +sloped +sloping +sloppily +sloppy +slot +slouching +slouchy +sludge +slug +slum +slurp +slush +sly +small +smartly +smartness +smasher +smashing +smashup +smell +smelting +smile +smilingly +smirk +smite +smith +smitten +smock +smog +smoked +smokeless +smokiness +smoking +smoky +smolder +smooth +smother +smudge +smudgy +smuggler +smuggling +smugly +smugness +snack +snagged +snaking +snap +snare +snarl +snazzy +sneak +sneer +sneeze +sneezing +snide +sniff +snippet +snipping +snitch +snooper +snooze +snore +snoring +snorkel +snort +snout +snowbird +snowboard +snowbound +snowcap +snowdrift +snowdrop +snowfall +snowfield +snowflake +snowiness +snowless +snowman +snowplow +snowshoe +snowstorm +snowsuit +snowy +snub +snuff +snuggle +snugly +snugness +speak +spearfish +spearhead +spearman +spearmint +species +specimen +specked +speckled +specks +spectacle +spectator +spectrum +speculate +speech +speed +spellbind +speller +spelling +spendable +spender +spending +spent +spew +sphere +spherical +sphinx +spider +spied +spiffy +spill +spilt +spinach +spinal +spindle +spinner +spinning +spinout +spinster +spiny +spiral +spirited +spiritism +spirits +spiritual +splashed +splashing +splashy +splatter +spleen +splendid +splendor +splice +splicing +splinter +splotchy +splurge +spoilage +spoiled +spoiler +spoiling +spoils +spoken +spokesman +sponge +spongy +sponsor +spoof +spookily +spooky +spool +spoon +spore +sporting +sports +sporty +spotless +spotlight +spotted +spotter +spotting +spotty +spousal +spouse +spout +sprain +sprang +sprawl +spray +spree +sprig +spring +sprinkled +sprinkler +sprint +sprite +sprout +spruce +sprung +spry +spud +spur +sputter +spyglass +squabble +squad +squall +squander +squash +squatted +squatter +squatting +squeak +squealer +squealing +squeamish +squeegee +squeeze +squeezing +squid +squiggle +squiggly +squint +squire +squirt +squishier +squishy +stability +stabilize +stable +stack +stadium +staff +stage +staging +stagnant +stagnate +stainable +stained +staining +stainless +stalemate +staleness +stalling +stallion +stamina +stammer +stamp +stand +stank +staple +stapling +starboard +starch +stardom +stardust +starfish +stargazer +staring +stark +starless +starlet +starlight +starlit +starring +starry +starship +starter +starting +startle +startling +startup +starved +starving +stash +state +static +statistic +statue +stature +status +statute +statutory +staunch +stays +steadfast +steadier +steadily +steadying +steam +steed +steep +steerable +steering +steersman +stegosaur +stellar +stem +stench +stencil +step +stereo +sterile +sterility +sterilize +sterling +sternness +sternum +stew +stick +stiffen +stiffly +stiffness +stifle +stifling +stillness +stilt +stimulant +stimulate +stimuli +stimulus +stinger +stingily +stinging +stingray +stingy +stinking +stinky +stipend +stipulate +stir +stitch +stock +stoic +stoke +stole +stomp +stonewall +stoneware +stonework +stoning +stony +stood +stooge +stool +stoop +stoplight +stoppable +stoppage +stopped +stopper +stopping +stopwatch +storable +storage +storeroom +storewide +storm +stout +stove +stowaway +stowing +straddle +straggler +strained +strainer +straining +strangely +stranger +strangle +strategic +strategy +stratus +straw +stray +streak +stream +street +strength +strenuous +strep +stress +stretch +strewn +stricken +strict +stride +strife +strike +striking +strive +striving +strobe +strode +stroller +strongbox +strongly +strongman +struck +structure +strudel +struggle +strum +strung +strut +stubbed +stubble +stubbly +stubborn +stucco +stuck +student +studied +studio +study +stuffed +stuffing +stuffy +stumble +stumbling +stump +stung +stunned +stunner +stunning +stunt +stupor +sturdily +sturdy +styling +stylishly +stylist +stylized +stylus +suave +subarctic +subatomic +subdivide +subdued +subduing +subfloor +subgroup +subheader +subject +sublease +sublet +sublevel +sublime +submarine +submerge +submersed +submitter +subpanel +subpar +subplot +subprime +subscribe +subscript +subsector +subside +subsiding +subsidize +subsidy +subsoil +subsonic +substance +subsystem +subtext +subtitle +subtly +subtotal +subtract +subtype +suburb +subway +subwoofer +subzero +succulent +such +suction +sudden +sudoku +suds +sufferer +suffering +suffice +suffix +suffocate +suffrage +sugar +suggest +suing +suitable +suitably +suitcase +suitor +sulfate +sulfide +sulfite +sulfur +sulk +sullen +sulphate +sulphuric +sultry +superbowl +superglue +superhero +superior +superjet +superman +supermom +supernova +supervise +supper +supplier +supply +support +supremacy +supreme +surcharge +surely +sureness +surface +surfacing +surfboard +surfer +surgery +surgical +surging +surname +surpass +surplus +surprise +surreal +surrender +surrogate +surround +survey +survival +survive +surviving +survivor +sushi +suspect +suspend +suspense +sustained +sustainer +swab +swaddling +swagger +swampland +swan +swapping +swarm +sway +swear +sweat +sweep +swell +swept +swerve +swifter +swiftly +swiftness +swimmable +swimmer +swimming +swimsuit +swimwear +swinger +swinging +swipe +swirl +switch +swivel +swizzle +swooned +swoop +swoosh +swore +sworn +swung +sycamore +sympathy +symphonic +symphony +symptom +synapse +syndrome +synergy +synopses +synopsis +synthesis +synthetic +syrup +system +t-shirt +tabasco +tabby +tableful +tables +tablet +tableware +tabloid +tackiness +tacking +tackle +tackling +tacky +taco +tactful +tactical +tactics +tactile +tactless +tadpole +taekwondo +tag +tainted +take +taking +talcum +talisman +tall +talon +tamale +tameness +tamer +tamper +tank +tanned +tannery +tanning +tantrum +tapeless +tapered +tapering +tapestry +tapioca +tapping +taps +tarantula +target +tarmac +tarnish +tarot +tartar +tartly +tartness +task +tassel +taste +tastiness +tasting +tasty +tattered +tattle +tattling +tattoo +taunt +tavern +thank +that +thaw +theater +theatrics +thee +theft +theme +theology +theorize +thermal +thermos +thesaurus +these +thesis +thespian +thicken +thicket +thickness +thieving +thievish +thigh +thimble +thing +think +thinly +thinner +thinness +thinning +thirstily +thirsting +thirsty +thirteen +thirty +thong +thorn +those +thousand +thrash +thread +threaten +threefold +thrift +thrill +thrive +thriving +throat +throbbing +throng +throttle +throwaway +throwback +thrower +throwing +thud +thumb +thumping +thursday +thus +thwarting +thyself +tiara +tibia +tidal +tidbit +tidiness +tidings +tidy +tiger +tighten +tightly +tightness +tightrope +tightwad +tigress +tile +tiling +till +tilt +timid +timing +timothy +tinderbox +tinfoil +tingle +tingling +tingly +tinker +tinkling +tinsel +tinsmith +tint +tinwork +tiny +tipoff +tipped +tipper +tipping +tiptoeing +tiptop +tiring +tissue +trace +tracing +track +traction +tractor +trade +trading +tradition +traffic +tragedy +trailing +trailside +train +traitor +trance +tranquil +transfer +transform +translate +transpire +transport +transpose +trapdoor +trapeze +trapezoid +trapped +trapper +trapping +traps +trash +travel +traverse +travesty +tray +treachery +treading +treadmill +treason +treat +treble +tree +trekker +tremble +trembling +tremor +trench +trend +trespass +triage +trial +triangle +tribesman +tribunal +tribune +tributary +tribute +triceps +trickery +trickily +tricking +trickle +trickster +tricky +tricolor +tricycle +trident +tried +trifle +trifocals +trillion +trilogy +trimester +trimmer +trimming +trimness +trinity +trio +tripod +tripping +triumph +trivial +trodden +trolling +trombone +trophy +tropical +tropics +trouble +troubling +trough +trousers +trout +trowel +truce +truck +truffle +trump +trunks +trustable +trustee +trustful +trusting +trustless +truth +try +tubby +tubeless +tubular +tucking +tuesday +tug +tuition +tulip +tumble +tumbling +tummy +turban +turbine +turbofan +turbojet +turbulent +turf +turkey +turmoil +turret +turtle +tusk +tutor +tutu +tux +tweak +tweed +tweet +tweezers +twelve +twentieth +twenty +twerp +twice +twiddle +twiddling +twig +twilight +twine +twins +twirl +twistable +twisted +twister +twisting +twisty +twitch +twitter +tycoon +tying +tyke +udder +ultimate +ultimatum +ultra +umbilical +umbrella +umpire +unabashed +unable +unadorned +unadvised +unafraid +unaired +unaligned +unaltered +unarmored +unashamed +unaudited +unawake +unaware +unbaked +unbalance +unbeaten +unbend +unbent +unbiased +unbitten +unblended +unblessed +unblock +unbolted +unbounded +unboxed +unbraided +unbridle +unbroken +unbuckled +unbundle +unburned +unbutton +uncanny +uncapped +uncaring +uncertain +unchain +unchanged +uncharted +uncheck +uncivil +unclad +unclaimed +unclamped +unclasp +uncle +unclip +uncloak +unclog +unclothed +uncoated +uncoiled +uncolored +uncombed +uncommon +uncooked +uncork +uncorrupt +uncounted +uncouple +uncouth +uncover +uncross +uncrown +uncrushed +uncured +uncurious +uncurled +uncut +undamaged +undated +undaunted +undead +undecided +undefined +underage +underarm +undercoat +undercook +undercut +underdog +underdone +underfed +underfeed +underfoot +undergo +undergrad +underhand +underline +underling +undermine +undermost +underpaid +underpass +underpay +underrate +undertake +undertone +undertook +undertow +underuse +underwear +underwent +underwire +undesired +undiluted +undivided +undocked +undoing +undone +undrafted +undress +undrilled +undusted +undying +unearned +unearth +unease +uneasily +uneasy +uneatable +uneaten +unedited +unelected +unending +unengaged +unenvied +unequal +unethical +uneven +unexpired +unexposed +unfailing +unfair +unfasten +unfazed +unfeeling +unfiled +unfilled +unfitted +unfitting +unfixable +unfixed +unflawed +unfocused +unfold +unfounded +unframed +unfreeze +unfrosted +unfrozen +unfunded +unglazed +ungloved +unglue +ungodly +ungraded +ungreased +unguarded +unguided +unhappily +unhappy +unharmed +unhealthy +unheard +unhearing +unheated +unhelpful +unhidden +unhinge +unhitched +unholy +unhook +unicorn +unicycle +unified +unifier +uniformed +uniformly +unify +unimpeded +uninjured +uninstall +uninsured +uninvited +union +uniquely +unisexual +unison +unissued +unit +universal +universe +unjustly +unkempt +unkind +unknotted +unknowing +unknown +unlaced +unlatch +unlawful +unleaded +unlearned +unleash +unless +unleveled +unlighted +unlikable +unlimited +unlined +unlinked +unlisted +unlit +unlivable +unloaded +unloader +unlocked +unlocking +unlovable +unloved +unlovely +unloving +unluckily +unlucky +unmade +unmanaged +unmanned +unmapped +unmarked +unmasked +unmasking +unmatched +unmindful +unmixable +unmixed +unmolded +unmoral +unmovable +unmoved +unmoving +unnamable +unnamed +unnatural +unneeded +unnerve +unnerving +unnoticed +unopened +unopposed +unpack +unpadded +unpaid +unpainted +unpaired +unpaved +unpeeled +unpicked +unpiloted +unpinned +unplanned +unplanted +unpleased +unpledged +unplowed +unplug +unpopular +unproven +unquote +unranked +unrated +unraveled +unreached +unread +unreal +unreeling +unrefined +unrelated +unrented +unrest +unretired +unrevised +unrigged +unripe +unrivaled +unroasted +unrobed +unroll +unruffled +unruly +unrushed +unsaddle +unsafe +unsaid +unsalted +unsaved +unsavory +unscathed +unscented +unscrew +unsealed +unseated +unsecured +unseeing +unseemly +unseen +unselect +unselfish +unsent +unsettled +unshackle +unshaken +unshaved +unshaven +unsheathe +unshipped +unsightly +unsigned +unskilled +unsliced +unsmooth +unsnap +unsocial +unsoiled +unsold +unsolved +unsorted +unspoiled +unspoken +unstable +unstaffed +unstamped +unsteady +unsterile +unstirred +unstitch +unstopped +unstuck +unstuffed +unstylish +unsubtle +unsubtly +unsuited +unsure +unsworn +untagged +untainted +untaken +untamed +untangled +untapped +untaxed +unthawed +unthread +untidy +untie +until +untimed +untimely +untitled +untoasted +untold +untouched +untracked +untrained +untreated +untried +untrimmed +untrue +untruth +unturned +untwist +untying +unusable +unused +unusual +unvalued +unvaried +unvarying +unveiled +unveiling +unvented +unviable +unvisited +unvocal +unwanted +unwarlike +unwary +unwashed +unwatched +unweave +unwed +unwelcome +unwell +unwieldy +unwilling +unwind +unwired +unwitting +unwomanly +unworldly +unworn +unworried +unworthy +unwound +unwoven +unwrapped +unwritten +unzip +upbeat +upchuck +upcoming +upcountry +update +upfront +upgrade +upheaval +upheld +uphill +uphold +uplifted +uplifting +upload +upon +upper +upright +uprising +upriver +uproar +uproot +upscale +upside +upstage +upstairs +upstart +upstate +upstream +upstroke +upswing +uptake +uptight +uptown +upturned +upward +upwind +uranium +urban +urchin +urethane +urgency +urgent +urging +urologist +urology +usable +usage +useable +used +uselessly +user +usher +usual +utensil +utility +utilize +utmost +utopia +utter +vacancy +vacant +vacate +vacation +vagabond +vagrancy +vagrantly +vaguely +vagueness +valiant +valid +valium +valley +valuables +value +vanilla +vanish +vanity +vanquish +vantage +vaporizer +variable +variably +varied +variety +various +varmint +varnish +varsity +varying +vascular +vaseline +vastly +vastness +veal +vegan +veggie +vehicular +velcro +velocity +velvet +vendetta +vending +vendor +veneering +vengeful +venomous +ventricle +venture +venue +venus +verbalize +verbally +verbose +verdict +verify +verse +version +versus +vertebrae +vertical +vertigo +very +vessel +vest +veteran +veto +vexingly +viability +viable +vibes +vice +vicinity +victory +video +viewable +viewer +viewing +viewless +viewpoint +vigorous +village +villain +vindicate +vineyard +vintage +violate +violation +violator +violet +violin +viper +viral +virtual +virtuous +virus +visa +viscosity +viscous +viselike +visible +visibly +vision +visiting +visitor +visor +vista +vitality +vitalize +vitally +vitamins +vivacious +vividly +vividness +vixen +vocalist +vocalize +vocally +vocation +voice +voicing +void +volatile +volley +voltage +volumes +voter +voting +voucher +vowed +vowel +voyage +wackiness +wad +wafer +waffle +waged +wager +wages +waggle +wagon +wake +waking +walk +walmart +walnut +walrus +waltz +wand +wannabe +wanted +wanting +wasabi +washable +washbasin +washboard +washbowl +washcloth +washday +washed +washer +washhouse +washing +washout +washroom +washstand +washtub +wasp +wasting +watch +water +waviness +waving +wavy +whacking +whacky +wham +wharf +wheat +whenever +whiff +whimsical +whinny +whiny +whisking +whoever +whole +whomever +whoopee +whooping +whoops +why +wick +widely +widen +widget +widow +width +wieldable +wielder +wife +wifi +wikipedia +wildcard +wildcat +wilder +wildfire +wildfowl +wildland +wildlife +wildly +wildness +willed +willfully +willing +willow +willpower +wilt +wimp +wince +wincing +wind +wing +winking +winner +winnings +winter +wipe +wired +wireless +wiring +wiry +wisdom +wise +wish +wisplike +wispy +wistful +wizard +wobble +wobbling +wobbly +wok +wolf +wolverine +womanhood +womankind +womanless +womanlike +womanly +womb +woof +wooing +wool +woozy +word +work +worried +worrier +worrisome +worry +worsening +worshiper +worst +wound +woven +wow +wrangle +wrath +wreath +wreckage +wrecker +wrecking +wrench +wriggle +wriggly +wrinkle +wrinkly +wrist +writing +written +wrongdoer +wronged +wrongful +wrongly +wrongness +wrought +xbox +xerox +yahoo +yam +yanking +yapping +yard +yarn +yeah +yearbook +yearling +yearly +yearning +yeast +yelling +yelp +yen +yesterday +yiddish +yield +yin +yippee +yo-yo +yodel +yoga +yogurt +yonder +yoyo +yummy +zap +zealous +zebra +zen +zeppelin +zero +zestfully +zesty +zigzagged +zipfile +zipping +zippy +zips +zit +zodiac +zombie +zone +zoning +zookeeper +zoologist +zoology +zoom diff --git a/ansible/lib/python3.11/site-packages/passlib/_data/wordsets/eff_prefixed.txt b/ansible/lib/python3.11/site-packages/passlib/_data/wordsets/eff_prefixed.txt new file mode 100644 index 000000000..9ac732fe3 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/_data/wordsets/eff_prefixed.txt @@ -0,0 +1,1296 @@ +aardvark +abandoned +abbreviate +abdomen +abhorrence +abiding +abnormal +abrasion +absorbing +abundant +abyss +academy +accountant +acetone +achiness +acid +acoustics +acquire +acrobat +actress +acuteness +aerosol +aesthetic +affidavit +afloat +afraid +aftershave +again +agency +aggressor +aghast +agitate +agnostic +agonizing +agreeing +aidless +aimlessly +ajar +alarmclock +albatross +alchemy +alfalfa +algae +aliens +alkaline +almanac +alongside +alphabet +already +also +altitude +aluminum +always +amazingly +ambulance +amendment +amiable +ammunition +amnesty +amoeba +amplifier +amuser +anagram +anchor +android +anesthesia +angelfish +animal +anklet +announcer +anonymous +answer +antelope +anxiety +anyplace +aorta +apartment +apnea +apostrophe +apple +apricot +aquamarine +arachnid +arbitrate +ardently +arena +argument +aristocrat +armchair +aromatic +arrowhead +arsonist +artichoke +asbestos +ascend +aseptic +ashamed +asinine +asleep +asocial +asparagus +astronaut +asymmetric +atlas +atmosphere +atom +atrocious +attic +atypical +auctioneer +auditorium +augmented +auspicious +automobile +auxiliary +avalanche +avenue +aviator +avocado +awareness +awhile +awkward +awning +awoke +axially +azalea +babbling +backpack +badass +bagpipe +bakery +balancing +bamboo +banana +barracuda +basket +bathrobe +bazooka +blade +blender +blimp +blouse +blurred +boatyard +bobcat +body +bogusness +bohemian +boiler +bonnet +boots +borough +bossiness +bottle +bouquet +boxlike +breath +briefcase +broom +brushes +bubblegum +buckle +buddhist +buffalo +bullfrog +bunny +busboy +buzzard +cabin +cactus +cadillac +cafeteria +cage +cahoots +cajoling +cakewalk +calculator +camera +canister +capsule +carrot +cashew +cathedral +caucasian +caviar +ceasefire +cedar +celery +cement +census +ceramics +cesspool +chalkboard +cheesecake +chimney +chlorine +chopsticks +chrome +chute +cilantro +cinnamon +circle +cityscape +civilian +clay +clergyman +clipboard +clock +clubhouse +coathanger +cobweb +coconut +codeword +coexistent +coffeecake +cognitive +cohabitate +collarbone +computer +confetti +copier +cornea +cosmetics +cotton +couch +coverless +coyote +coziness +crawfish +crewmember +crib +croissant +crumble +crystal +cubical +cucumber +cuddly +cufflink +cuisine +culprit +cup +curry +cushion +cuticle +cybernetic +cyclist +cylinder +cymbal +cynicism +cypress +cytoplasm +dachshund +daffodil +dagger +dairy +dalmatian +dandelion +dartboard +dastardly +datebook +daughter +dawn +daytime +dazzler +dealer +debris +decal +dedicate +deepness +defrost +degree +dehydrator +deliverer +democrat +dentist +deodorant +depot +deranged +desktop +detergent +device +dexterity +diamond +dibs +dictionary +diffuser +digit +dilated +dimple +dinnerware +dioxide +diploma +directory +dishcloth +ditto +dividers +dizziness +doctor +dodge +doll +dominoes +donut +doorstep +dorsal +double +downstairs +dozed +drainpipe +dresser +driftwood +droppings +drum +dryer +dubiously +duckling +duffel +dugout +dumpster +duplex +durable +dustpan +dutiful +duvet +dwarfism +dwelling +dwindling +dynamite +dyslexia +eagerness +earlobe +easel +eavesdrop +ebook +eccentric +echoless +eclipse +ecosystem +ecstasy +edged +editor +educator +eelworm +eerie +effects +eggnog +egomaniac +ejection +elastic +elbow +elderly +elephant +elfishly +eliminator +elk +elliptical +elongated +elsewhere +elusive +elves +emancipate +embroidery +emcee +emerald +emission +emoticon +emperor +emulate +enactment +enchilada +endorphin +energy +enforcer +engine +enhance +enigmatic +enjoyably +enlarged +enormous +enquirer +enrollment +ensemble +entryway +enunciate +envoy +enzyme +epidemic +equipment +erasable +ergonomic +erratic +eruption +escalator +eskimo +esophagus +espresso +essay +estrogen +etching +eternal +ethics +etiquette +eucalyptus +eulogy +euphemism +euthanize +evacuation +evergreen +evidence +evolution +exam +excerpt +exerciser +exfoliate +exhale +exist +exorcist +explode +exquisite +exterior +exuberant +fabric +factory +faded +failsafe +falcon +family +fanfare +fasten +faucet +favorite +feasibly +february +federal +feedback +feigned +feline +femur +fence +ferret +festival +fettuccine +feudalist +feverish +fiberglass +fictitious +fiddle +figurine +fillet +finalist +fiscally +fixture +flashlight +fleshiness +flight +florist +flypaper +foamless +focus +foggy +folksong +fondue +footpath +fossil +fountain +fox +fragment +freeway +fridge +frosting +fruit +fryingpan +gadget +gainfully +gallstone +gamekeeper +gangway +garlic +gaslight +gathering +gauntlet +gearbox +gecko +gem +generator +geographer +gerbil +gesture +getaway +geyser +ghoulishly +gibberish +giddiness +giftshop +gigabyte +gimmick +giraffe +giveaway +gizmo +glasses +gleeful +glisten +glove +glucose +glycerin +gnarly +gnomish +goatskin +goggles +goldfish +gong +gooey +gorgeous +gosling +gothic +gourmet +governor +grape +greyhound +grill +groundhog +grumbling +guacamole +guerrilla +guitar +gullible +gumdrop +gurgling +gusto +gutless +gymnast +gynecology +gyration +habitat +hacking +haggard +haiku +halogen +hamburger +handgun +happiness +hardhat +hastily +hatchling +haughty +hazelnut +headband +hedgehog +hefty +heinously +helmet +hemoglobin +henceforth +herbs +hesitation +hexagon +hubcap +huddling +huff +hugeness +hullabaloo +human +hunter +hurricane +hushing +hyacinth +hybrid +hydrant +hygienist +hypnotist +ibuprofen +icepack +icing +iconic +identical +idiocy +idly +igloo +ignition +iguana +illuminate +imaging +imbecile +imitator +immigrant +imprint +iodine +ionosphere +ipad +iphone +iridescent +irksome +iron +irrigation +island +isotope +issueless +italicize +itemizer +itinerary +itunes +ivory +jabbering +jackrabbit +jaguar +jailhouse +jalapeno +jamboree +janitor +jarring +jasmine +jaundice +jawbreaker +jaywalker +jazz +jealous +jeep +jelly +jeopardize +jersey +jetski +jezebel +jiffy +jigsaw +jingling +jobholder +jockstrap +jogging +john +joinable +jokingly +journal +jovial +joystick +jubilant +judiciary +juggle +juice +jujitsu +jukebox +jumpiness +junkyard +juror +justifying +juvenile +kabob +kamikaze +kangaroo +karate +kayak +keepsake +kennel +kerosene +ketchup +khaki +kickstand +kilogram +kimono +kingdom +kiosk +kissing +kite +kleenex +knapsack +kneecap +knickers +koala +krypton +laboratory +ladder +lakefront +lantern +laptop +laryngitis +lasagna +latch +laundry +lavender +laxative +lazybones +lecturer +leftover +leggings +leisure +lemon +length +leopard +leprechaun +lettuce +leukemia +levers +lewdness +liability +library +licorice +lifeboat +lightbulb +likewise +lilac +limousine +lint +lioness +lipstick +liquid +listless +litter +liverwurst +lizard +llama +luau +lubricant +lucidity +ludicrous +luggage +lukewarm +lullaby +lumberjack +lunchbox +luridness +luscious +luxurious +lyrics +macaroni +maestro +magazine +mahogany +maimed +majority +makeover +malformed +mammal +mango +mapmaker +marbles +massager +matchstick +maverick +maximum +mayonnaise +moaning +mobilize +moccasin +modify +moisture +molecule +momentum +monastery +moonshine +mortuary +mosquito +motorcycle +mousetrap +movie +mower +mozzarella +muckiness +mudflow +mugshot +mule +mummy +mundane +muppet +mural +mustard +mutation +myriad +myspace +myth +nail +namesake +nanosecond +napkin +narrator +nastiness +natives +nautically +navigate +nearest +nebula +nectar +nefarious +negotiator +neither +nemesis +neoliberal +nephew +nervously +nest +netting +neuron +nevermore +nextdoor +nicotine +niece +nimbleness +nintendo +nirvana +nuclear +nugget +nuisance +nullify +numbing +nuptials +nursery +nutcracker +nylon +oasis +oat +obediently +obituary +object +obliterate +obnoxious +observer +obtain +obvious +occupation +oceanic +octopus +ocular +office +oftentimes +oiliness +ointment +older +olympics +omissible +omnivorous +oncoming +onion +onlooker +onstage +onward +onyx +oomph +opaquely +opera +opium +opossum +opponent +optical +opulently +oscillator +osmosis +ostrich +otherwise +ought +outhouse +ovation +oven +owlish +oxford +oxidize +oxygen +oyster +ozone +pacemaker +padlock +pageant +pajamas +palm +pamphlet +pantyhose +paprika +parakeet +passport +patio +pauper +pavement +payphone +pebble +peculiarly +pedometer +pegboard +pelican +penguin +peony +pepperoni +peroxide +pesticide +petroleum +pewter +pharmacy +pheasant +phonebook +phrasing +physician +plank +pledge +plotted +plug +plywood +pneumonia +podiatrist +poetic +pogo +poison +poking +policeman +poncho +popcorn +porcupine +postcard +poultry +powerboat +prairie +pretzel +princess +propeller +prune +pry +pseudo +psychopath +publisher +pucker +pueblo +pulley +pumpkin +punchbowl +puppy +purse +pushup +putt +puzzle +pyramid +python +quarters +quesadilla +quilt +quote +racoon +radish +ragweed +railroad +rampantly +rancidity +rarity +raspberry +ravishing +rearrange +rebuilt +receipt +reentry +refinery +register +rehydrate +reimburse +rejoicing +rekindle +relic +remote +renovator +reopen +reporter +request +rerun +reservoir +retriever +reunion +revolver +rewrite +rhapsody +rhetoric +rhino +rhubarb +rhyme +ribbon +riches +ridden +rigidness +rimmed +riptide +riskily +ritzy +riverboat +roamer +robe +rocket +romancer +ropelike +rotisserie +roundtable +royal +rubber +rudderless +rugby +ruined +rulebook +rummage +running +rupture +rustproof +sabotage +sacrifice +saddlebag +saffron +sainthood +saltshaker +samurai +sandworm +sapphire +sardine +sassy +satchel +sauna +savage +saxophone +scarf +scenario +schoolbook +scientist +scooter +scrapbook +sculpture +scythe +secretary +sedative +segregator +seismology +selected +semicolon +senator +septum +sequence +serpent +sesame +settler +severely +shack +shelf +shirt +shovel +shrimp +shuttle +shyness +siamese +sibling +siesta +silicon +simmering +singles +sisterhood +sitcom +sixfold +sizable +skateboard +skeleton +skies +skulk +skylight +slapping +sled +slingshot +sloth +slumbering +smartphone +smelliness +smitten +smokestack +smudge +snapshot +sneezing +sniff +snowsuit +snugness +speakers +sphinx +spider +splashing +sponge +sprout +spur +spyglass +squirrel +statue +steamboat +stingray +stopwatch +strawberry +student +stylus +suave +subway +suction +suds +suffocate +sugar +suitcase +sulphur +superstore +surfer +sushi +swan +sweatshirt +swimwear +sword +sycamore +syllable +symphony +synagogue +syringes +systemize +tablespoon +taco +tadpole +taekwondo +tagalong +takeout +tallness +tamale +tanned +tapestry +tarantula +tastebud +tattoo +tavern +thaw +theater +thimble +thorn +throat +thumb +thwarting +tiara +tidbit +tiebreaker +tiger +timid +tinsel +tiptoeing +tirade +tissue +tractor +tree +tripod +trousers +trucks +tryout +tubeless +tuesday +tugboat +tulip +tumbleweed +tupperware +turtle +tusk +tutorial +tuxedo +tweezers +twins +tyrannical +ultrasound +umbrella +umpire +unarmored +unbuttoned +uncle +underwear +unevenness +unflavored +ungloved +unhinge +unicycle +unjustly +unknown +unlocking +unmarked +unnoticed +unopened +unpaved +unquenched +unroll +unscrewing +untied +unusual +unveiled +unwrinkled +unyielding +unzip +upbeat +upcountry +update +upfront +upgrade +upholstery +upkeep +upload +uppercut +upright +upstairs +uptown +upwind +uranium +urban +urchin +urethane +urgent +urologist +username +usher +utensil +utility +utmost +utopia +utterance +vacuum +vagrancy +valuables +vanquished +vaporizer +varied +vaseline +vegetable +vehicle +velcro +vendor +vertebrae +vestibule +veteran +vexingly +vicinity +videogame +viewfinder +vigilante +village +vinegar +violin +viperfish +virus +visor +vitamins +vivacious +vixen +vocalist +vogue +voicemail +volleyball +voucher +voyage +vulnerable +waffle +wagon +wakeup +walrus +wanderer +wasp +water +waving +wheat +whisper +wholesaler +wick +widow +wielder +wifeless +wikipedia +wildcat +windmill +wipeout +wired +wishbone +wizardry +wobbliness +wolverine +womb +woolworker +workbasket +wound +wrangle +wreckage +wristwatch +wrongdoing +xerox +xylophone +yacht +yahoo +yard +yearbook +yesterday +yiddish +yield +yo-yo +yodel +yogurt +yuppie +zealot +zebra +zeppelin +zestfully +zigzagged +zillion +zipping +zirconium +zodiac +zombie +zookeeper +zucchini diff --git a/ansible/lib/python3.11/site-packages/passlib/_data/wordsets/eff_short.txt b/ansible/lib/python3.11/site-packages/passlib/_data/wordsets/eff_short.txt new file mode 100644 index 000000000..4c8baa4ce --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/_data/wordsets/eff_short.txt @@ -0,0 +1,1296 @@ +acid +acorn +acre +acts +afar +affix +aged +agent +agile +aging +agony +ahead +aide +aids +aim +ajar +alarm +alias +alibi +alien +alike +alive +aloe +aloft +aloha +alone +amend +amino +ample +amuse +angel +anger +angle +ankle +apple +april +apron +aqua +area +arena +argue +arise +armed +armor +army +aroma +array +arson +art +ashen +ashes +atlas +atom +attic +audio +avert +avoid +awake +award +awoke +axis +bacon +badge +bagel +baggy +baked +baker +balmy +banjo +barge +barn +bash +basil +bask +batch +bath +baton +bats +blade +blank +blast +blaze +bleak +blend +bless +blimp +blink +bloat +blob +blog +blot +blunt +blurt +blush +boast +boat +body +boil +bok +bolt +boned +boney +bonus +bony +book +booth +boots +boss +botch +both +boxer +breed +bribe +brick +bride +brim +bring +brink +brisk +broad +broil +broke +brook +broom +brush +buck +bud +buggy +bulge +bulk +bully +bunch +bunny +bunt +bush +bust +busy +buzz +cable +cache +cadet +cage +cake +calm +cameo +canal +candy +cane +canon +cape +card +cargo +carol +carry +carve +case +cash +cause +cedar +chain +chair +chant +chaos +charm +chase +cheek +cheer +chef +chess +chest +chew +chief +chili +chill +chip +chomp +chop +chow +chuck +chump +chunk +churn +chute +cider +cinch +city +civic +civil +clad +claim +clamp +clap +clash +clasp +class +claw +clay +clean +clear +cleat +cleft +clerk +click +cling +clink +clip +cloak +clock +clone +cloth +cloud +clump +coach +coast +coat +cod +coil +coke +cola +cold +colt +coma +come +comic +comma +cone +cope +copy +coral +cork +cost +cot +couch +cough +cover +cozy +craft +cramp +crane +crank +crate +crave +crawl +crazy +creme +crepe +crept +crib +cried +crisp +crook +crop +cross +crowd +crown +crumb +crush +crust +cub +cult +cupid +cure +curl +curry +curse +curve +curvy +cushy +cut +cycle +dab +dad +daily +dairy +daisy +dance +dandy +darn +dart +dash +data +date +dawn +deaf +deal +dean +debit +debt +debug +decaf +decal +decay +deck +decor +decoy +deed +delay +denim +dense +dent +depth +derby +desk +dial +diary +dice +dig +dill +dime +dimly +diner +dingy +disco +dish +disk +ditch +ditzy +dizzy +dock +dodge +doing +doll +dome +donor +donut +dose +dot +dove +down +dowry +doze +drab +drama +drank +draw +dress +dried +drift +drill +drive +drone +droop +drove +drown +drum +dry +duck +duct +dude +dug +duke +duo +dusk +dust +duty +dwarf +dwell +eagle +early +earth +easel +east +eaten +eats +ebay +ebony +ebook +echo +edge +eel +eject +elbow +elder +elf +elk +elm +elope +elude +elves +email +emit +empty +emu +enter +entry +envoy +equal +erase +error +erupt +essay +etch +evade +even +evict +evil +evoke +exact +exit +fable +faced +fact +fade +fall +false +fancy +fang +fax +feast +feed +femur +fence +fend +ferry +fetal +fetch +fever +fiber +fifth +fifty +film +filth +final +finch +fit +five +flag +flaky +flame +flap +flask +fled +flick +fling +flint +flip +flirt +float +flock +flop +floss +flyer +foam +foe +fog +foil +folic +folk +food +fool +found +fox +foyer +frail +frame +fray +fresh +fried +frill +frisk +from +front +frost +froth +frown +froze +fruit +gag +gains +gala +game +gap +gas +gave +gear +gecko +geek +gem +genre +gift +gig +gills +given +giver +glad +glass +glide +gloss +glove +glow +glue +goal +going +golf +gong +good +gooey +goofy +gore +gown +grab +grain +grant +grape +graph +grasp +grass +grave +gravy +gray +green +greet +grew +grid +grief +grill +grip +grit +groom +grope +growl +grub +grunt +guide +gulf +gulp +gummy +guru +gush +gut +guy +habit +half +halo +halt +happy +harm +hash +hasty +hatch +hate +haven +hazel +hazy +heap +heat +heave +hedge +hefty +help +herbs +hers +hub +hug +hula +hull +human +humid +hump +hung +hunk +hunt +hurry +hurt +hush +hut +ice +icing +icon +icy +igloo +image +ion +iron +islam +issue +item +ivory +ivy +jab +jam +jaws +jazz +jeep +jelly +jet +jiffy +job +jog +jolly +jolt +jot +joy +judge +juice +juicy +july +jumbo +jump +junky +juror +jury +keep +keg +kept +kick +kilt +king +kite +kitty +kiwi +knee +knelt +koala +kung +ladle +lady +lair +lake +lance +land +lapel +large +lash +lasso +last +latch +late +lazy +left +legal +lemon +lend +lens +lent +level +lever +lid +life +lift +lilac +lily +limb +limes +line +lint +lion +lip +list +lived +liver +lunar +lunch +lung +lurch +lure +lurk +lying +lyric +mace +maker +malt +mama +mango +manor +many +map +march +mardi +marry +mash +match +mate +math +moan +mocha +moist +mold +mom +moody +mop +morse +most +motor +motto +mount +mouse +mousy +mouth +move +movie +mower +mud +mug +mulch +mule +mull +mumbo +mummy +mural +muse +music +musky +mute +nacho +nag +nail +name +nanny +nap +navy +near +neat +neon +nerd +nest +net +next +niece +ninth +nutty +oak +oasis +oat +ocean +oil +old +olive +omen +onion +only +ooze +opal +open +opera +opt +otter +ouch +ounce +outer +oval +oven +owl +ozone +pace +pagan +pager +palm +panda +panic +pants +panty +paper +park +party +pasta +patch +path +patio +payer +pecan +penny +pep +perch +perky +perm +pest +petal +petri +petty +photo +plank +plant +plaza +plead +plot +plow +pluck +plug +plus +poach +pod +poem +poet +pogo +point +poise +poker +polar +polio +polka +polo +pond +pony +poppy +pork +poser +pouch +pound +pout +power +prank +press +print +prior +prism +prize +probe +prong +proof +props +prude +prune +pry +pug +pull +pulp +pulse +puma +punch +punk +pupil +puppy +purr +purse +push +putt +quack +quake +query +quiet +quill +quilt +quit +quota +quote +rabid +race +rack +radar +radio +raft +rage +raid +rail +rake +rally +ramp +ranch +range +rank +rant +rash +raven +reach +react +ream +rebel +recap +relax +relay +relic +remix +repay +repel +reply +rerun +reset +rhyme +rice +rich +ride +rigid +rigor +rinse +riot +ripen +rise +risk +ritzy +rival +river +roast +robe +robin +rock +rogue +roman +romp +rope +rover +royal +ruby +rug +ruin +rule +runny +rush +rust +rut +sadly +sage +said +saint +salad +salon +salsa +salt +same +sandy +santa +satin +sauna +saved +savor +sax +say +scale +scam +scan +scare +scarf +scary +scoff +scold +scoop +scoot +scope +score +scorn +scout +scowl +scrap +scrub +scuba +scuff +sect +sedan +self +send +sepia +serve +set +seven +shack +shade +shady +shaft +shaky +sham +shape +share +sharp +shed +sheep +sheet +shelf +shell +shine +shiny +ship +shirt +shock +shop +shore +shout +shove +shown +showy +shred +shrug +shun +shush +shut +shy +sift +silk +silly +silo +sip +siren +sixth +size +skate +skew +skid +skier +skies +skip +skirt +skit +sky +slab +slack +slain +slam +slang +slash +slate +slaw +sled +sleek +sleep +sleet +slept +slice +slick +slimy +sling +slip +slit +slob +slot +slug +slum +slurp +slush +small +smash +smell +smile +smirk +smog +snack +snap +snare +snarl +sneak +sneer +sniff +snore +snort +snout +snowy +snub +snuff +speak +speed +spend +spent +spew +spied +spill +spiny +spoil +spoke +spoof +spool +spoon +sport +spot +spout +spray +spree +spur +squad +squat +squid +stack +staff +stage +stain +stall +stamp +stand +stank +stark +start +stash +state +stays +steam +steep +stem +step +stew +stick +sting +stir +stock +stole +stomp +stony +stood +stool +stoop +stop +storm +stout +stove +straw +stray +strut +stuck +stud +stuff +stump +stung +stunt +suds +sugar +sulk +surf +sushi +swab +swan +swarm +sway +swear +sweat +sweep +swell +swept +swim +swing +swipe +swirl +swoop +swore +syrup +tacky +taco +tag +take +tall +talon +tamer +tank +taper +taps +tarot +tart +task +taste +tasty +taunt +thank +thaw +theft +theme +thigh +thing +think +thong +thorn +those +throb +thud +thumb +thump +thus +tiara +tidal +tidy +tiger +tile +tilt +tint +tiny +trace +track +trade +train +trait +trap +trash +tray +treat +tree +trek +trend +trial +tribe +trick +trio +trout +truce +truck +trump +trunk +try +tug +tulip +tummy +turf +tusk +tutor +tutu +tux +tweak +tweet +twice +twine +twins +twirl +twist +uncle +uncut +undo +unify +union +unit +untie +upon +upper +urban +used +user +usher +utter +value +vapor +vegan +venue +verse +vest +veto +vice +video +view +viral +virus +visa +visor +vixen +vocal +voice +void +volt +voter +vowel +wad +wafer +wager +wages +wagon +wake +walk +wand +wasp +watch +water +wavy +wheat +whiff +whole +whoop +wick +widen +widow +width +wife +wifi +wilt +wimp +wind +wing +wink +wipe +wired +wiry +wise +wish +wispy +wok +wolf +womb +wool +woozy +word +work +worry +wound +woven +wrath +wreck +wrist +xerox +yahoo +yam +yard +year +yeast +yelp +yield +yo-yo +yodel +yoga +yoyo +yummy +zebra +zero +zesty +zippy +zone +zoom diff --git a/ansible/lib/python3.11/site-packages/passlib/apache.py b/ansible/lib/python3.11/site-packages/passlib/apache.py new file mode 100644 index 000000000..a75f2cf3c --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/apache.py @@ -0,0 +1,1255 @@ +"""passlib.apache - apache password support""" +# XXX: relocate this to passlib.ext.apache? +#============================================================================= +# imports +#============================================================================= +from __future__ import with_statement +# core +import logging; log = logging.getLogger(__name__) +import os +from warnings import warn +# site +# pkg +from passlib import exc, registry +from passlib.context import CryptContext +from passlib.exc import ExpectedStringError +from passlib.hash import htdigest +from passlib.utils import render_bytes, to_bytes, is_ascii_codec +from passlib.utils.decor import deprecated_method +from passlib.utils.compat import join_bytes, unicode, BytesIO, PY3 +# local +__all__ = [ + 'HtpasswdFile', + 'HtdigestFile', +] + +#============================================================================= +# constants & support +#============================================================================= +_UNSET = object() + +_BCOLON = b":" +_BHASH = b"#" + +# byte values that aren't allowed in fields. +_INVALID_FIELD_CHARS = b":\n\r\t\x00" + +#: _CommonFile._source token types +_SKIPPED = "skipped" +_RECORD = "record" + +#============================================================================= +# common helpers +#============================================================================= +class _CommonFile(object): + """common framework for HtpasswdFile & HtdigestFile""" + #=================================================================== + # instance attrs + #=================================================================== + + # charset encoding used by file (defaults to utf-8) + encoding = None + + # whether users() and other public methods should return unicode or bytes? + # (defaults to False under PY2, True under PY3) + return_unicode = None + + # if bound to local file, these will be set. + _path = None # local file path + _mtime = None # mtime when last loaded, or 0 + + # if true, automatically save to local file after changes are made. + autosave = False + + # dict mapping key -> value for all records in database. + # (e.g. user => hash for Htpasswd) + _records = None + + #: list of tokens for recreating original file contents when saving. if present, + #: will be sequence of (_SKIPPED, b"whitespace/comments") and (_RECORD, ) tuples. + _source = None + + #=================================================================== + # alt constuctors + #=================================================================== + @classmethod + def from_string(cls, data, **kwds): + """create new object from raw string. + + :type data: unicode or bytes + :arg data: + database to load, as single string. + + :param \\*\\*kwds: + all other keywords are the same as in the class constructor + """ + if 'path' in kwds: + raise TypeError("'path' not accepted by from_string()") + self = cls(**kwds) + self.load_string(data) + return self + + @classmethod + def from_path(cls, path, **kwds): + """create new object from file, without binding object to file. + + :type path: str + :arg path: + local filepath to load from + + :param \\*\\*kwds: + all other keywords are the same as in the class constructor + """ + self = cls(**kwds) + self.load(path) + return self + + #=================================================================== + # init + #=================================================================== + def __init__(self, path=None, new=False, autoload=True, autosave=False, + encoding="utf-8", return_unicode=PY3, + ): + # set encoding + if not encoding: + warn("``encoding=None`` is deprecated as of Passlib 1.6, " + "and will cause a ValueError in Passlib 1.8, " + "use ``return_unicode=False`` instead.", + DeprecationWarning, stacklevel=2) + encoding = "utf-8" + return_unicode = False + elif not is_ascii_codec(encoding): + # htpasswd/htdigest files assumes 1-byte chars, and use ":" separator, + # so only ascii-compatible encodings are allowed. + raise ValueError("encoding must be 7-bit ascii compatible") + self.encoding = encoding + + # set other attrs + self.return_unicode = return_unicode + self.autosave = autosave + self._path = path + self._mtime = 0 + + # init db + if not autoload: + warn("``autoload=False`` is deprecated as of Passlib 1.6, " + "and will be removed in Passlib 1.8, use ``new=True`` instead", + DeprecationWarning, stacklevel=2) + new = True + if path and not new: + self.load() + else: + self._records = {} + self._source = [] + + def __repr__(self): + tail = '' + if self.autosave: + tail += ' autosave=True' + if self._path: + tail += ' path=%r' % self._path + if self.encoding != "utf-8": + tail += ' encoding=%r' % self.encoding + return "<%s 0x%0x%s>" % (self.__class__.__name__, id(self), tail) + + # NOTE: ``path`` is a property so that ``_mtime`` is wiped when it's set. + + @property + def path(self): + return self._path + + @path.setter + def path(self, value): + if value != self._path: + self._mtime = 0 + self._path = value + + @property + def mtime(self): + """modify time when last loaded (if bound to a local file)""" + return self._mtime + + #=================================================================== + # loading + #=================================================================== + def load_if_changed(self): + """Reload from ``self.path`` only if file has changed since last load""" + if not self._path: + raise RuntimeError("%r is not bound to a local file" % self) + if self._mtime and self._mtime == os.path.getmtime(self._path): + return False + self.load() + return True + + def load(self, path=None, force=True): + """Load state from local file. + If no path is specified, attempts to load from ``self.path``. + + :type path: str + :arg path: local file to load from + + :type force: bool + :param force: + if ``force=False``, only load from ``self.path`` if file + has changed since last load. + + .. deprecated:: 1.6 + This keyword will be removed in Passlib 1.8; + Applications should use :meth:`load_if_changed` instead. + """ + if path is not None: + with open(path, "rb") as fh: + self._mtime = 0 + self._load_lines(fh) + elif not force: + warn("%(name)s.load(force=False) is deprecated as of Passlib 1.6," + "and will be removed in Passlib 1.8; " + "use %(name)s.load_if_changed() instead." % + dict(name=self.__class__.__name__), + DeprecationWarning, stacklevel=2) + return self.load_if_changed() + elif self._path: + with open(self._path, "rb") as fh: + self._mtime = os.path.getmtime(self._path) + self._load_lines(fh) + else: + raise RuntimeError("%s().path is not set, an explicit path is required" % + self.__class__.__name__) + return True + + def load_string(self, data): + """Load state from unicode or bytes string, replacing current state""" + data = to_bytes(data, self.encoding, "data") + self._mtime = 0 + self._load_lines(BytesIO(data)) + + def _load_lines(self, lines): + """load from sequence of lists""" + parse = self._parse_record + records = {} + source = [] + skipped = b'' + for idx, line in enumerate(lines): + # NOTE: per htpasswd source (https://github.com/apache/httpd/blob/trunk/support/htpasswd.c), + # lines with only whitespace, or with "#" as first non-whitespace char, + # are left alone / ignored. + tmp = line.lstrip() + if not tmp or tmp.startswith(_BHASH): + skipped += line + continue + + # parse valid line + key, value = parse(line, idx+1) + + # NOTE: if multiple entries for a key, we use the first one, + # which seems to match htpasswd source + if key in records: + log.warning("username occurs multiple times in source file: %r" % key) + skipped += line + continue + + # flush buffer of skipped whitespace lines + if skipped: + source.append((_SKIPPED, skipped)) + skipped = b'' + + # store new user line + records[key] = value + source.append((_RECORD, key)) + + # don't bother preserving trailing whitespace, but do preserve trailing comments + if skipped.rstrip(): + source.append((_SKIPPED, skipped)) + + # NOTE: not replacing ._records until parsing succeeds, so loading is atomic. + self._records = records + self._source = source + + def _parse_record(self, record, lineno): # pragma: no cover - abstract method + """parse line of file into (key, value) pair""" + raise NotImplementedError("should be implemented in subclass") + + def _set_record(self, key, value): + """ + helper for setting record which takes care of inserting source line if needed; + + :returns: + bool if key already present + """ + records = self._records + existing = (key in records) + records[key] = value + if not existing: + self._source.append((_RECORD, key)) + return existing + + #=================================================================== + # saving + #=================================================================== + def _autosave(self): + """subclass helper to call save() after any changes""" + if self.autosave and self._path: + self.save() + + def save(self, path=None): + """Save current state to file. + If no path is specified, attempts to save to ``self.path``. + """ + if path is not None: + with open(path, "wb") as fh: + fh.writelines(self._iter_lines()) + elif self._path: + self.save(self._path) + self._mtime = os.path.getmtime(self._path) + else: + raise RuntimeError("%s().path is not set, cannot autosave" % + self.__class__.__name__) + + def to_string(self): + """Export current state as a string of bytes""" + return join_bytes(self._iter_lines()) + + # def clean(self): + # """ + # discard any comments or whitespace that were being preserved from the source file, + # and re-sort keys in alphabetical order + # """ + # self._source = [(_RECORD, key) for key in sorted(self._records)] + # self._autosave() + + def _iter_lines(self): + """iterator yielding lines of database""" + # NOTE: this relies on being an OrderedDict so that it outputs + # records in a deterministic order. + records = self._records + if __debug__: + pending = set(records) + for action, content in self._source: + if action == _SKIPPED: + # 'content' is whitespace/comments to write + yield content + else: + assert action == _RECORD + # 'content' is record key + if content not in records: + # record was deleted + # NOTE: doing it lazily like this so deleting & re-adding user + # preserves their original location in the file. + continue + yield self._render_record(content, records[content]) + if __debug__: + pending.remove(content) + if __debug__: + # sanity check that we actually wrote all the records + # (otherwise _source & _records are somehow out of sync) + assert not pending, "failed to write all records: missing=%r" % (pending,) + + def _render_record(self, key, value): # pragma: no cover - abstract method + """given key/value pair, encode as line of file""" + raise NotImplementedError("should be implemented in subclass") + + #=================================================================== + # field encoding + #=================================================================== + def _encode_user(self, user): + """user-specific wrapper for _encode_field()""" + return self._encode_field(user, "user") + + def _encode_realm(self, realm): # pragma: no cover - abstract method + """realm-specific wrapper for _encode_field()""" + return self._encode_field(realm, "realm") + + def _encode_field(self, value, param="field"): + """convert field to internal representation. + + internal representation is always bytes. byte strings are left as-is, + unicode strings encoding using file's default encoding (or ``utf-8`` + if no encoding has been specified). + + :raises UnicodeEncodeError: + if unicode value cannot be encoded using default encoding. + + :raises ValueError: + if resulting byte string contains a forbidden character, + or is too long (>255 bytes). + + :returns: + encoded identifer as bytes + """ + if isinstance(value, unicode): + value = value.encode(self.encoding) + elif not isinstance(value, bytes): + raise ExpectedStringError(value, param) + if len(value) > 255: + raise ValueError("%s must be at most 255 characters: %r" % + (param, value)) + if any(c in _INVALID_FIELD_CHARS for c in value): + raise ValueError("%s contains invalid characters: %r" % + (param, value,)) + return value + + def _decode_field(self, value): + """decode field from internal representation to format + returns by users() method, etc. + + :raises UnicodeDecodeError: + if unicode value cannot be decoded using default encoding. + (usually indicates wrong encoding set for file). + + :returns: + field as unicode or bytes, as appropriate. + """ + assert isinstance(value, bytes), "expected value to be bytes" + if self.return_unicode: + return value.decode(self.encoding) + else: + return value + + # FIXME: htpasswd doc says passwords limited to 255 chars under Windows & MPE, + # and that longer ones are truncated. this may be side-effect of those + # platforms supporting the 'plaintext' scheme. these classes don't currently + # check for this. + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# htpasswd context +# +# This section sets up a CryptContexts to mimic what schemes Apache +# (and the htpasswd tool) should support on the current system. +# +# Apache has long-time supported some basic builtin schemes (listed below), +# as well as the host's crypt() method -- though it's limited to being able +# to *verify* any scheme using that method, but can only generate "des_crypt" hashes. +# +# Apache 2.4 added builtin bcrypt support (even for platforms w/o native support). +# c.f. http://httpd.apache.org/docs/2.4/programs/htpasswd.html vs the 2.2 docs. +#============================================================================= + +#: set of default schemes that (if chosen) should be using bcrypt, +#: but can't due to lack of bcrypt. +_warn_no_bcrypt = set() + +def _init_default_schemes(): + + #: pick strongest one for host + host_best = None + for name in ["bcrypt", "sha256_crypt"]: + if registry.has_os_crypt_support(name): + host_best = name + break + + # check if we have a bcrypt backend -- otherwise issue warning + # XXX: would like to not spam this unless the user *requests* apache 24 + bcrypt = "bcrypt" if registry.has_backend("bcrypt") else None + _warn_no_bcrypt.clear() + if not bcrypt: + _warn_no_bcrypt.update(["portable_apache_24", "host_apache_24", + "linux_apache_24", "portable", "host"]) + + defaults = dict( + # strongest hash builtin to specific apache version + portable_apache_24=bcrypt or "apr_md5_crypt", + portable_apache_22="apr_md5_crypt", + + # strongest hash across current host & specific apache version + host_apache_24=bcrypt or host_best or "apr_md5_crypt", + host_apache_22=host_best or "apr_md5_crypt", + + # strongest hash on a linux host + linux_apache_24=bcrypt or "sha256_crypt", + linux_apache_22="sha256_crypt", + ) + + # set latest-apache version aliases + # XXX: could check for apache install, and pick correct host 22/24 default? + # could reuse _detect_htpasswd() helper in UTs + defaults.update( + portable=defaults['portable_apache_24'], + host=defaults['host_apache_24'], + ) + return defaults + +#: dict mapping default alias -> appropriate scheme +htpasswd_defaults = _init_default_schemes() + +def _init_htpasswd_context(): + + # start with schemes built into apache + schemes = [ + # builtin support added in apache 2.4 + # (https://bz.apache.org/bugzilla/show_bug.cgi?id=49288) + "bcrypt", + + # support not "builtin" to apache, instead it requires support through host's crypt(). + # adding them here to allow editing htpasswd under windows and then deploying under unix. + "sha256_crypt", + "sha512_crypt", + "des_crypt", + + # apache default as of 2.2.18, and still default in 2.4 + "apr_md5_crypt", + + # NOTE: apache says ONLY intended for transitioning htpasswd <-> ldap + "ldap_sha1", + + # NOTE: apache says ONLY supported on Windows, Netware, TPF + "plaintext" + ] + + # apache can verify anything supported by the native crypt(), + # though htpasswd tool can only generate a limited set of hashes. + # (this list may overlap w/ builtin apache schemes) + schemes.extend(registry.get_supported_os_crypt_schemes()) + + # hack to remove dups and sort into preferred order + preferred = schemes[:3] + ["apr_md5_crypt"] + schemes + schemes = sorted(set(schemes), key=preferred.index) + + # create context object + return CryptContext( + schemes=schemes, + + # NOTE: default will change to "portable" in passlib 2.0 + default=htpasswd_defaults['portable_apache_22'], + + # NOTE: bcrypt "2y" is required, "2b" isn't recognized by libapr (issue 95) + bcrypt__ident="2y", + ) + +#: CryptContext configured to match htpasswd +htpasswd_context = _init_htpasswd_context() + +#============================================================================= +# htpasswd editing +#============================================================================= + +class HtpasswdFile(_CommonFile): + """class for reading & writing Htpasswd files. + + The class constructor accepts the following arguments: + + :type path: filepath + :param path: + + Specifies path to htpasswd file, use to implicitly load from and save to. + + This class has two modes of operation: + + 1. It can be "bound" to a local file by passing a ``path`` to the class + constructor. In this case it will load the contents of the file when + created, and the :meth:`load` and :meth:`save` methods will automatically + load from and save to that file if they are called without arguments. + + 2. Alternately, it can exist as an independant object, in which case + :meth:`load` and :meth:`save` will require an explicit path to be + provided whenever they are called. As well, ``autosave`` behavior + will not be available. + + This feature is new in Passlib 1.6, and is the default if no + ``path`` value is provided to the constructor. + + This is also exposed as a readonly instance attribute. + + :type new: bool + :param new: + + Normally, if *path* is specified, :class:`HtpasswdFile` will + immediately load the contents of the file. However, when creating + a new htpasswd file, applications can set ``new=True`` so that + the existing file (if any) will not be loaded. + + .. versionadded:: 1.6 + This feature was previously enabled by setting ``autoload=False``. + That alias has been deprecated, and will be removed in Passlib 1.8 + + :type autosave: bool + :param autosave: + + Normally, any changes made to an :class:`HtpasswdFile` instance + will not be saved until :meth:`save` is explicitly called. However, + if ``autosave=True`` is specified, any changes made will be + saved to disk immediately (assuming *path* has been set). + + This is also exposed as a writeable instance attribute. + + :type encoding: str + :param encoding: + + Optionally specify character encoding used to read/write file + and hash passwords. Defaults to ``utf-8``, though ``latin-1`` + is the only other commonly encountered encoding. + + This is also exposed as a readonly instance attribute. + + :type default_scheme: str + :param default_scheme: + Optionally specify default scheme to use when encoding new passwords. + + This can be any of the schemes with builtin Apache support, + OR natively supported by the host OS's :func:`crypt.crypt` function. + + * Builtin schemes include ``"bcrypt"`` (apache 2.4+), ``"apr_md5_crypt"`, + and ``"des_crypt"``. + + * Schemes commonly supported by Unix hosts + include ``"bcrypt"``, ``"sha256_crypt"``, and ``"des_crypt"``. + + In order to not have to sort out what you should use, + passlib offers a number of aliases, that will resolve + to the most appropriate scheme based on your needs: + + * ``"portable"``, ``"portable_apache_24"`` -- pick scheme that's portable across hosts + running apache >= 2.4. **This will be the default as of Passlib 2.0**. + + * ``"portable_apache_22"`` -- pick scheme that's portable across hosts + running apache >= 2.4. **This is the default up to Passlib 1.9**. + + * ``"host"``, ``"host_apache_24"`` -- pick strongest scheme supported by + apache >= 2.4 and/or host OS. + + * ``"host_apache_22"`` -- pick strongest scheme supported by + apache >= 2.2 and/or host OS. + + .. versionadded:: 1.6 + This keyword was previously named ``default``. That alias + has been deprecated, and will be removed in Passlib 1.8. + + .. versionchanged:: 1.6.3 + + Added support for ``"bcrypt"``, ``"sha256_crypt"``, and ``"portable"`` alias. + + .. versionchanged:: 1.7 + + Added apache 2.4 semantics, and additional aliases. + + :type context: :class:`~passlib.context.CryptContext` + :param context: + :class:`!CryptContext` instance used to create + and verify the hashes found in the htpasswd file. + The default value is a pre-built context which supports all + of the hashes officially allowed in an htpasswd file. + + This is also exposed as a readonly instance attribute. + + .. warning:: + + This option may be used to add support for non-standard hash + formats to an htpasswd file. However, the resulting file + will probably not be usable by another application, + and particularly not by Apache. + + :param autoload: + Set to ``False`` to prevent the constructor from automatically + loaded the file from disk. + + .. deprecated:: 1.6 + This has been replaced by the *new* keyword. + Instead of setting ``autoload=False``, you should use + ``new=True``. Support for this keyword will be removed + in Passlib 1.8. + + :param default: + Change the default algorithm used to hash new passwords. + + .. deprecated:: 1.6 + This has been renamed to *default_scheme* for clarity. + Support for this alias will be removed in Passlib 1.8. + + Loading & Saving + ================ + .. automethod:: load + .. automethod:: load_if_changed + .. automethod:: load_string + .. automethod:: save + .. automethod:: to_string + + Inspection + ================ + .. automethod:: users + .. automethod:: check_password + .. automethod:: get_hash + + Modification + ================ + .. automethod:: set_password + .. automethod:: delete + + Alternate Constructors + ====================== + .. automethod:: from_string + + Attributes + ========== + .. attribute:: path + + Path to local file that will be used as the default + for all :meth:`load` and :meth:`save` operations. + May be written to, initialized by the *path* constructor keyword. + + .. attribute:: autosave + + Writeable flag indicating whether changes will be automatically + written to *path*. + + Errors + ====== + :raises ValueError: + All of the methods in this class will raise a :exc:`ValueError` if + any user name contains a forbidden character (one of ``:\\r\\n\\t\\x00``), + or is longer than 255 characters. + """ + #=================================================================== + # instance attrs + #=================================================================== + + # NOTE: _records map stores for the key, and for the value, + # both in bytes which use self.encoding + + #=================================================================== + # init & serialization + #=================================================================== + def __init__(self, path=None, default_scheme=None, context=htpasswd_context, + **kwds): + if 'default' in kwds: + warn("``default`` is deprecated as of Passlib 1.6, " + "and will be removed in Passlib 1.8, it has been renamed " + "to ``default_scheem``.", + DeprecationWarning, stacklevel=2) + default_scheme = kwds.pop("default") + if default_scheme: + if default_scheme in _warn_no_bcrypt: + warn("HtpasswdFile: no bcrypt backends available, " + "using fallback for default scheme %r" % default_scheme, + exc.PasslibSecurityWarning) + default_scheme = htpasswd_defaults.get(default_scheme, default_scheme) + context = context.copy(default=default_scheme) + self.context = context + super(HtpasswdFile, self).__init__(path, **kwds) + + def _parse_record(self, record, lineno): + # NOTE: should return (user, hash) tuple + result = record.rstrip().split(_BCOLON) + if len(result) != 2: + raise ValueError("malformed htpasswd file (error reading line %d)" + % lineno) + return result + + def _render_record(self, user, hash): + return render_bytes("%s:%s\n", user, hash) + + #=================================================================== + # public methods + #=================================================================== + + def users(self): + """ + Return list of all users in database + """ + return [self._decode_field(user) for user in self._records] + + ##def has_user(self, user): + ## "check whether entry is present for user" + ## return self._encode_user(user) in self._records + + ##def rename(self, old, new): + ## """rename user account""" + ## old = self._encode_user(old) + ## new = self._encode_user(new) + ## hash = self._records.pop(old) + ## self._records[new] = hash + ## self._autosave() + + def set_password(self, user, password): + """Set password for user; adds user if needed. + + :returns: + * ``True`` if existing user was updated. + * ``False`` if user account was added. + + .. versionchanged:: 1.6 + This method was previously called ``update``, it was renamed + to prevent ambiguity with the dictionary method. + The old alias is deprecated, and will be removed in Passlib 1.8. + """ + hash = self.context.hash(password) + return self.set_hash(user, hash) + + @deprecated_method(deprecated="1.6", removed="1.8", + replacement="set_password") + def update(self, user, password): + """set password for user""" + return self.set_password(user, password) + + def get_hash(self, user): + """Return hash stored for user, or ``None`` if user not found. + + .. versionchanged:: 1.6 + This method was previously named ``find``, it was renamed + for clarity. The old name is deprecated, and will be removed + in Passlib 1.8. + """ + try: + return self._records[self._encode_user(user)] + except KeyError: + return None + + def set_hash(self, user, hash): + """ + semi-private helper which allows writing a hash directly; + adds user if needed. + + .. warning:: + does not (currently) do any validation of the hash string + + .. versionadded:: 1.7 + """ + # assert self.context.identify(hash), "unrecognized hash format" + if PY3 and isinstance(hash, str): + hash = hash.encode(self.encoding) + user = self._encode_user(user) + existing = self._set_record(user, hash) + self._autosave() + return existing + + @deprecated_method(deprecated="1.6", removed="1.8", + replacement="get_hash") + def find(self, user): + """return hash for user""" + return self.get_hash(user) + + # XXX: rename to something more explicit, like delete_user()? + def delete(self, user): + """Delete user's entry. + + :returns: + * ``True`` if user deleted. + * ``False`` if user not found. + """ + try: + del self._records[self._encode_user(user)] + except KeyError: + return False + self._autosave() + return True + + def check_password(self, user, password): + """ + Verify password for specified user. + If algorithm marked as deprecated by CryptContext, will automatically be re-hashed. + + :returns: + * ``None`` if user not found. + * ``False`` if user found, but password does not match. + * ``True`` if user found and password matches. + + .. versionchanged:: 1.6 + This method was previously called ``verify``, it was renamed + to prevent ambiguity with the :class:`!CryptContext` method. + The old alias is deprecated, and will be removed in Passlib 1.8. + """ + user = self._encode_user(user) + hash = self._records.get(user) + if hash is None: + return None + if isinstance(password, unicode): + # NOTE: encoding password to match file, making the assumption + # that server will use same encoding to hash the password. + password = password.encode(self.encoding) + ok, new_hash = self.context.verify_and_update(password, hash) + if ok and new_hash is not None: + # rehash user's password if old hash was deprecated + assert user in self._records # otherwise would have to use ._set_record() + self._records[user] = new_hash + self._autosave() + return ok + + @deprecated_method(deprecated="1.6", removed="1.8", + replacement="check_password") + def verify(self, user, password): + """verify password for user""" + return self.check_password(user, password) + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# htdigest editing +#============================================================================= +class HtdigestFile(_CommonFile): + """class for reading & writing Htdigest files. + + The class constructor accepts the following arguments: + + :type path: filepath + :param path: + + Specifies path to htdigest file, use to implicitly load from and save to. + + This class has two modes of operation: + + 1. It can be "bound" to a local file by passing a ``path`` to the class + constructor. In this case it will load the contents of the file when + created, and the :meth:`load` and :meth:`save` methods will automatically + load from and save to that file if they are called without arguments. + + 2. Alternately, it can exist as an independant object, in which case + :meth:`load` and :meth:`save` will require an explicit path to be + provided whenever they are called. As well, ``autosave`` behavior + will not be available. + + This feature is new in Passlib 1.6, and is the default if no + ``path`` value is provided to the constructor. + + This is also exposed as a readonly instance attribute. + + :type default_realm: str + :param default_realm: + + If ``default_realm`` is set, all the :class:`HtdigestFile` + methods that require a realm will use this value if one is not + provided explicitly. If unset, they will raise an error stating + that an explicit realm is required. + + This is also exposed as a writeable instance attribute. + + .. versionadded:: 1.6 + + :type new: bool + :param new: + + Normally, if *path* is specified, :class:`HtdigestFile` will + immediately load the contents of the file. However, when creating + a new htpasswd file, applications can set ``new=True`` so that + the existing file (if any) will not be loaded. + + .. versionadded:: 1.6 + This feature was previously enabled by setting ``autoload=False``. + That alias has been deprecated, and will be removed in Passlib 1.8 + + :type autosave: bool + :param autosave: + + Normally, any changes made to an :class:`HtdigestFile` instance + will not be saved until :meth:`save` is explicitly called. However, + if ``autosave=True`` is specified, any changes made will be + saved to disk immediately (assuming *path* has been set). + + This is also exposed as a writeable instance attribute. + + :type encoding: str + :param encoding: + + Optionally specify character encoding used to read/write file + and hash passwords. Defaults to ``utf-8``, though ``latin-1`` + is the only other commonly encountered encoding. + + This is also exposed as a readonly instance attribute. + + :param autoload: + Set to ``False`` to prevent the constructor from automatically + loaded the file from disk. + + .. deprecated:: 1.6 + This has been replaced by the *new* keyword. + Instead of setting ``autoload=False``, you should use + ``new=True``. Support for this keyword will be removed + in Passlib 1.8. + + Loading & Saving + ================ + .. automethod:: load + .. automethod:: load_if_changed + .. automethod:: load_string + .. automethod:: save + .. automethod:: to_string + + Inspection + ========== + .. automethod:: realms + .. automethod:: users + .. automethod:: check_password(user[, realm], password) + .. automethod:: get_hash + + Modification + ============ + .. automethod:: set_password(user[, realm], password) + .. automethod:: delete + .. automethod:: delete_realm + + Alternate Constructors + ====================== + .. automethod:: from_string + + Attributes + ========== + .. attribute:: default_realm + + The default realm that will be used if one is not provided + to methods that require it. By default this is ``None``, + in which case an explicit realm must be provided for every + method call. Can be written to. + + .. attribute:: path + + Path to local file that will be used as the default + for all :meth:`load` and :meth:`save` operations. + May be written to, initialized by the *path* constructor keyword. + + .. attribute:: autosave + + Writeable flag indicating whether changes will be automatically + written to *path*. + + Errors + ====== + :raises ValueError: + All of the methods in this class will raise a :exc:`ValueError` if + any user name or realm contains a forbidden character (one of ``:\\r\\n\\t\\x00``), + or is longer than 255 characters. + """ + #=================================================================== + # instance attrs + #=================================================================== + + # NOTE: _records map stores (,) for the key, + # and as the value, all as bytes. + + # NOTE: unlike htpasswd, this class doesn't use a CryptContext, + # as only one hash format is supported: htdigest. + + # optionally specify default realm that will be used if none + # is provided to a method call. otherwise realm is always required. + default_realm = None + + #=================================================================== + # init & serialization + #=================================================================== + def __init__(self, path=None, default_realm=None, **kwds): + self.default_realm = default_realm + super(HtdigestFile, self).__init__(path, **kwds) + + def _parse_record(self, record, lineno): + result = record.rstrip().split(_BCOLON) + if len(result) != 3: + raise ValueError("malformed htdigest file (error reading line %d)" + % lineno) + user, realm, hash = result + return (user, realm), hash + + def _render_record(self, key, hash): + user, realm = key + return render_bytes("%s:%s:%s\n", user, realm, hash) + + def _require_realm(self, realm): + if realm is None: + realm = self.default_realm + if realm is None: + raise TypeError("you must specify a realm explicitly, " + "or set the default_realm attribute") + return realm + + def _encode_realm(self, realm): + realm = self._require_realm(realm) + return self._encode_field(realm, "realm") + + def _encode_key(self, user, realm): + return self._encode_user(user), self._encode_realm(realm) + + #=================================================================== + # public methods + #=================================================================== + + def realms(self): + """Return list of all realms in database""" + realms = set(key[1] for key in self._records) + return [self._decode_field(realm) for realm in realms] + + def users(self, realm=None): + """Return list of all users in specified realm. + + * uses ``self.default_realm`` if no realm explicitly provided. + * returns empty list if realm not found. + """ + realm = self._encode_realm(realm) + return [self._decode_field(key[0]) for key in self._records + if key[1] == realm] + + ##def has_user(self, user, realm=None): + ## "check if user+realm combination exists" + ## return self._encode_key(user,realm) in self._records + + ##def rename_realm(self, old, new): + ## """rename all accounts in realm""" + ## old = self._encode_realm(old) + ## new = self._encode_realm(new) + ## keys = [key for key in self._records if key[1] == old] + ## for key in keys: + ## hash = self._records.pop(key) + ## self._set_record((key[0], new), hash) + ## self._autosave() + ## return len(keys) + + ##def rename(self, old, new, realm=None): + ## """rename user account""" + ## old = self._encode_user(old) + ## new = self._encode_user(new) + ## realm = self._encode_realm(realm) + ## hash = self._records.pop((old,realm)) + ## self._set_record((new, realm), hash) + ## self._autosave() + + def set_password(self, user, realm=None, password=_UNSET): + """Set password for user; adds user & realm if needed. + + If ``self.default_realm`` has been set, this may be called + with the syntax ``set_password(user, password)``, + otherwise it must be called with all three arguments: + ``set_password(user, realm, password)``. + + :returns: + * ``True`` if existing user was updated + * ``False`` if user account added. + """ + if password is _UNSET: + # called w/ two args - (user, password), use default realm + realm, password = None, realm + realm = self._require_realm(realm) + hash = htdigest.hash(password, user, realm, encoding=self.encoding) + return self.set_hash(user, realm, hash) + + @deprecated_method(deprecated="1.6", removed="1.8", + replacement="set_password") + def update(self, user, realm, password): + """set password for user""" + return self.set_password(user, realm, password) + + def get_hash(self, user, realm=None): + """Return :class:`~passlib.hash.htdigest` hash stored for user. + + * uses ``self.default_realm`` if no realm explicitly provided. + * returns ``None`` if user or realm not found. + + .. versionchanged:: 1.6 + This method was previously named ``find``, it was renamed + for clarity. The old name is deprecated, and will be removed + in Passlib 1.8. + """ + key = self._encode_key(user, realm) + hash = self._records.get(key) + if hash is None: + return None + if PY3: + hash = hash.decode(self.encoding) + return hash + + def set_hash(self, user, realm=None, hash=_UNSET): + """ + semi-private helper which allows writing a hash directly; + adds user & realm if needed. + + If ``self.default_realm`` has been set, this may be called + with the syntax ``set_hash(user, hash)``, + otherwise it must be called with all three arguments: + ``set_hash(user, realm, hash)``. + + .. warning:: + does not (currently) do any validation of the hash string + + .. versionadded:: 1.7 + """ + if hash is _UNSET: + # called w/ two args - (user, hash), use default realm + realm, hash = None, realm + # assert htdigest.identify(hash), "unrecognized hash format" + if PY3 and isinstance(hash, str): + hash = hash.encode(self.encoding) + key = self._encode_key(user, realm) + existing = self._set_record(key, hash) + self._autosave() + return existing + + @deprecated_method(deprecated="1.6", removed="1.8", + replacement="get_hash") + def find(self, user, realm): + """return hash for user""" + return self.get_hash(user, realm) + + # XXX: rename to something more explicit, like delete_user()? + def delete(self, user, realm=None): + """Delete user's entry for specified realm. + + if realm is not specified, uses ``self.default_realm``. + + :returns: + * ``True`` if user deleted, + * ``False`` if user not found in realm. + """ + key = self._encode_key(user, realm) + try: + del self._records[key] + except KeyError: + return False + self._autosave() + return True + + def delete_realm(self, realm): + """Delete all users for specified realm. + + if realm is not specified, uses ``self.default_realm``. + + :returns: number of users deleted (0 if realm not found) + """ + realm = self._encode_realm(realm) + records = self._records + keys = [key for key in records if key[1] == realm] + for key in keys: + del records[key] + self._autosave() + return len(keys) + + def check_password(self, user, realm=None, password=_UNSET): + """Verify password for specified user + realm. + + If ``self.default_realm`` has been set, this may be called + with the syntax ``check_password(user, password)``, + otherwise it must be called with all three arguments: + ``check_password(user, realm, password)``. + + :returns: + * ``None`` if user or realm not found. + * ``False`` if user found, but password does not match. + * ``True`` if user found and password matches. + + .. versionchanged:: 1.6 + This method was previously called ``verify``, it was renamed + to prevent ambiguity with the :class:`!CryptContext` method. + The old alias is deprecated, and will be removed in Passlib 1.8. + """ + if password is _UNSET: + # called w/ two args - (user, password), use default realm + realm, password = None, realm + user = self._encode_user(user) + realm = self._encode_realm(realm) + hash = self._records.get((user,realm)) + if hash is None: + return None + return htdigest.verify(password, hash, user, realm, + encoding=self.encoding) + + @deprecated_method(deprecated="1.6", removed="1.8", + replacement="check_password") + def verify(self, user, realm, password): + """verify password for user""" + return self.check_password(user, realm, password) + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/apps.py b/ansible/lib/python3.11/site-packages/passlib/apps.py new file mode 100644 index 000000000..682bbff6f --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/apps.py @@ -0,0 +1,245 @@ +"""passlib.apps""" +#============================================================================= +# imports +#============================================================================= +# core +import logging; log = logging.getLogger(__name__) +from itertools import chain +# site +# pkg +from passlib import hash +from passlib.context import LazyCryptContext +from passlib.utils import sys_bits +# local +__all__ = [ + 'custom_app_context', + 'django_context', + 'ldap_context', 'ldap_nocrypt_context', + 'mysql_context', 'mysql4_context', 'mysql3_context', + 'phpass_context', + 'phpbb3_context', + 'postgres_context', +] + +#============================================================================= +# master containing all identifiable hashes +#============================================================================= +def _load_master_config(): + from passlib.registry import list_crypt_handlers + + # get master list + schemes = list_crypt_handlers() + + # exclude the ones we know have ambiguous or greedy identify() methods. + excluded = [ + # frequently confused for eachother + 'bigcrypt', + 'crypt16', + + # no good identifiers + 'cisco_pix', + 'cisco_type7', + 'htdigest', + 'mysql323', + 'oracle10', + + # all have same size + 'lmhash', + 'msdcc', + 'msdcc2', + 'nthash', + + # plaintext handlers + 'plaintext', + 'ldap_plaintext', + + # disabled handlers + 'django_disabled', + 'unix_disabled', + 'unix_fallback', + ] + for name in excluded: + schemes.remove(name) + + # return config + return dict(schemes=schemes, default="sha256_crypt") +master_context = LazyCryptContext(onload=_load_master_config) + +#============================================================================= +# for quickly bootstrapping new custom applications +#============================================================================= +custom_app_context = LazyCryptContext( + # choose some reasonbly strong schemes + schemes=["sha512_crypt", "sha256_crypt"], + + # set some useful global options + default="sha256_crypt" if sys_bits < 64 else "sha512_crypt", + + # set a good starting point for rounds selection + sha512_crypt__min_rounds = 535000, + sha256_crypt__min_rounds = 535000, + + # if the admin user category is selected, make a much stronger hash, + admin__sha512_crypt__min_rounds = 1024000, + admin__sha256_crypt__min_rounds = 1024000, + ) + +#============================================================================= +# django +#============================================================================= + +#----------------------------------------------------------------------- +# 1.0 +#----------------------------------------------------------------------- + +_django10_schemes = [ + "django_salted_sha1", + "django_salted_md5", + "django_des_crypt", + "hex_md5", + "django_disabled", +] + +django10_context = LazyCryptContext( + schemes=_django10_schemes, + default="django_salted_sha1", + deprecated=["hex_md5"], +) + +#----------------------------------------------------------------------- +# 1.4 +#----------------------------------------------------------------------- + +_django14_schemes = [ + "django_pbkdf2_sha256", + "django_pbkdf2_sha1", + "django_bcrypt" +] + _django10_schemes + +django14_context = LazyCryptContext( + schemes=_django14_schemes, + deprecated=_django10_schemes, +) + +#----------------------------------------------------------------------- +# 1.6 +#----------------------------------------------------------------------- + +_django16_schemes = list(_django14_schemes) +_django16_schemes.insert(1, "django_bcrypt_sha256") +django16_context = LazyCryptContext( + schemes=_django16_schemes, + deprecated=_django10_schemes, +) + +#----------------------------------------------------------------------- +# 1.10 +#----------------------------------------------------------------------- + +_django_110_schemes = [ + "django_pbkdf2_sha256", + "django_pbkdf2_sha1", + "django_argon2", + "django_bcrypt", + "django_bcrypt_sha256", + "django_disabled", +] +django110_context = LazyCryptContext(schemes=_django_110_schemes) + +#----------------------------------------------------------------------- +# 2.1 +#----------------------------------------------------------------------- + +_django21_schemes = list(_django_110_schemes) +_django21_schemes.remove("django_bcrypt") +django21_context = LazyCryptContext(schemes=_django21_schemes) + +#----------------------------------------------------------------------- +# latest +#----------------------------------------------------------------------- + +# this will always point to latest version in passlib +django_context = django21_context + +#============================================================================= +# ldap +#============================================================================= + +#: standard ldap schemes +std_ldap_schemes = [ + "ldap_salted_sha512", + "ldap_salted_sha256", + "ldap_salted_sha1", + "ldap_salted_md5", + "ldap_sha1", + "ldap_md5", + "ldap_plaintext", +] + +# create context with all std ldap schemes EXCEPT crypt +ldap_nocrypt_context = LazyCryptContext(std_ldap_schemes) + +# create context with all possible std ldap + ldap crypt schemes +def _iter_ldap_crypt_schemes(): + from passlib.utils import unix_crypt_schemes + return ('ldap_' + name for name in unix_crypt_schemes) + +def _iter_ldap_schemes(): + """helper which iterates over supported std ldap schemes""" + return chain(std_ldap_schemes, _iter_ldap_crypt_schemes()) +ldap_context = LazyCryptContext(_iter_ldap_schemes()) + +### create context with all std ldap schemes + crypt schemes for localhost +##def _iter_host_ldap_schemes(): +## "helper which iterates over supported std ldap schemes" +## from passlib.handlers.ldap_digests import get_host_ldap_crypt_schemes +## return chain(std_ldap_schemes, get_host_ldap_crypt_schemes()) +##ldap_host_context = LazyCryptContext(_iter_host_ldap_schemes()) + +#============================================================================= +# mysql +#============================================================================= +mysql3_context = LazyCryptContext(["mysql323"]) +mysql4_context = LazyCryptContext(["mysql41", "mysql323"], deprecated="mysql323") +mysql_context = mysql4_context # tracks latest mysql version supported + +#============================================================================= +# postgres +#============================================================================= +postgres_context = LazyCryptContext(["postgres_md5"]) + +#============================================================================= +# phpass & variants +#============================================================================= +def _create_phpass_policy(**kwds): + """helper to choose default alg based on bcrypt availability""" + kwds['default'] = 'bcrypt' if hash.bcrypt.has_backend() else 'phpass' + return kwds + +phpass_context = LazyCryptContext( + schemes=["bcrypt", "phpass", "bsdi_crypt"], + onload=_create_phpass_policy, + ) + +phpbb3_context = LazyCryptContext(["phpass"], phpass__ident="H") + +# TODO: support the drupal phpass variants (see phpass homepage) + +#============================================================================= +# roundup +#============================================================================= + +_std_roundup_schemes = [ "ldap_hex_sha1", "ldap_hex_md5", "ldap_des_crypt", "roundup_plaintext" ] +roundup10_context = LazyCryptContext(_std_roundup_schemes) + +# NOTE: 'roundup15' really applies to roundup 1.4.17+ +roundup_context = roundup15_context = LazyCryptContext( + schemes=_std_roundup_schemes + [ "ldap_pbkdf2_sha1" ], + deprecated=_std_roundup_schemes, + default = "ldap_pbkdf2_sha1", + ldap_pbkdf2_sha1__default_rounds = 10000, + ) + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/context.py b/ansible/lib/python3.11/site-packages/passlib/context.py new file mode 100644 index 000000000..bc3cbf50f --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/context.py @@ -0,0 +1,2637 @@ +"""passlib.context - CryptContext implementation""" +#============================================================================= +# imports +#============================================================================= +from __future__ import with_statement +# core +import re +import logging; log = logging.getLogger(__name__) +import threading +import time +from warnings import warn +# site +# pkg +from passlib import exc +from passlib.exc import ExpectedStringError, ExpectedTypeError, PasslibConfigWarning +from passlib.registry import get_crypt_handler, _validate_handler_name +from passlib.utils import (handlers as uh, to_bytes, + to_unicode, splitcomma, + as_bool, timer, rng, getrandstr, + ) +from passlib.utils.binary import BASE64_CHARS +from passlib.utils.compat import (iteritems, num_types, irange, + PY2, PY3, unicode, SafeConfigParser, + NativeStringIO, BytesIO, + unicode_or_bytes_types, native_string_types, + ) +from passlib.utils.decor import deprecated_method, memoized_property +# local +__all__ = [ + 'CryptContext', + 'LazyCryptContext', + 'CryptPolicy', +] + +#============================================================================= +# support +#============================================================================= + +# private object to detect unset params +_UNSET = object() + +def _coerce_vary_rounds(value): + """parse vary_rounds string to percent as [0,1) float, or integer""" + if value.endswith("%"): + # XXX: deprecate this in favor of raw float? + return float(value.rstrip("%"))*.01 + try: + return int(value) + except ValueError: + return float(value) + +# set of options which aren't allowed to be set via policy +_forbidden_scheme_options = set(["salt"]) + # 'salt' - not allowed since a fixed salt would defeat the purpose. + +# dict containing funcs used to coerce strings to correct type for scheme option keys. +# NOTE: this isn't really needed any longer, since Handler.using() handles the actual parsing. +# keeping this around for now, though, since it makes context.to_dict() output cleaner. +_coerce_scheme_options = dict( + min_rounds=int, + max_rounds=int, + default_rounds=int, + vary_rounds=_coerce_vary_rounds, + salt_size=int, +) + +def _is_handler_registered(handler): + """detect if handler is registered or a custom handler""" + return get_crypt_handler(handler.name, None) is handler + +@staticmethod +def _always_needs_update(hash, secret=None): + """ + dummy function patched into handler.needs_update() by _CryptConfig + when hash alg has been deprecated for context. + """ + return True + +#: list of keys allowed under wildcard "all" scheme w/o a security warning. +_global_settings = set(["truncate_error", "vary_rounds"]) + +#============================================================================= +# crypt policy +#============================================================================= +_preamble = ("The CryptPolicy class has been deprecated as of " + "Passlib 1.6, and will be removed in Passlib 1.8. ") + +class CryptPolicy(object): + """ + .. deprecated:: 1.6 + This class has been deprecated, and will be removed in Passlib 1.8. + All of its functionality has been rolled into :class:`CryptContext`. + + This class previously stored the configuration options for the + CryptContext class. In the interest of interface simplification, + all of this class' functionality has been rolled into the CryptContext + class itself. + The documentation for this class is now focused on documenting how to + migrate to the new api. Additionally, where possible, the deprecation + warnings issued by the CryptPolicy methods will list the replacement call + that should be used. + + Constructors + ============ + CryptPolicy objects can be constructed directly using any of + the keywords accepted by :class:`CryptContext`. Direct uses of the + :class:`!CryptPolicy` constructor should either pass the keywords + directly into the CryptContext constructor, or to :meth:`CryptContext.update` + if the policy object was being used to update an existing context object. + + In addition to passing in keywords directly, + CryptPolicy objects can be constructed by the following methods: + + .. automethod:: from_path + .. automethod:: from_string + .. automethod:: from_source + .. automethod:: from_sources + .. automethod:: replace + + Introspection + ============= + All of the informational methods provided by this class have been deprecated + by identical or similar methods in the :class:`CryptContext` class: + + .. automethod:: has_schemes + .. automethod:: schemes + .. automethod:: iter_handlers + .. automethod:: get_handler + .. automethod:: get_options + .. automethod:: handler_is_deprecated + .. automethod:: get_min_verify_time + + Exporting + ========= + .. automethod:: iter_config + .. automethod:: to_dict + .. automethod:: to_file + .. automethod:: to_string + + .. note:: + CryptPolicy are immutable. + Use the :meth:`replace` method to mutate existing instances. + + .. deprecated:: 1.6 + """ + #=================================================================== + # class methods + #=================================================================== + @classmethod + def from_path(cls, path, section="passlib", encoding="utf-8"): + """create a CryptPolicy instance from a local file. + + .. deprecated:: 1.6 + + Creating a new CryptContext from a file, which was previously done via + ``CryptContext(policy=CryptPolicy.from_path(path))``, can now be + done via ``CryptContext.from_path(path)``. + See :meth:`CryptContext.from_path` for details. + + Updating an existing CryptContext from a file, which was previously done + ``context.policy = CryptPolicy.from_path(path)``, can now be + done via ``context.load_path(path)``. + See :meth:`CryptContext.load_path` for details. + """ + warn(_preamble + + "Instead of ``CryptPolicy.from_path(path)``, " + "use ``CryptContext.from_path(path)`` " + " or ``context.load_path(path)`` for an existing CryptContext.", + DeprecationWarning, stacklevel=2) + return cls(_internal_context=CryptContext.from_path(path, section, + encoding)) + + @classmethod + def from_string(cls, source, section="passlib", encoding="utf-8"): + """create a CryptPolicy instance from a string. + + .. deprecated:: 1.6 + + Creating a new CryptContext from a string, which was previously done via + ``CryptContext(policy=CryptPolicy.from_string(data))``, can now be + done via ``CryptContext.from_string(data)``. + See :meth:`CryptContext.from_string` for details. + + Updating an existing CryptContext from a string, which was previously done + ``context.policy = CryptPolicy.from_string(data)``, can now be + done via ``context.load(data)``. + See :meth:`CryptContext.load` for details. + """ + warn(_preamble + + "Instead of ``CryptPolicy.from_string(source)``, " + "use ``CryptContext.from_string(source)`` or " + "``context.load(source)`` for an existing CryptContext.", + DeprecationWarning, stacklevel=2) + return cls(_internal_context=CryptContext.from_string(source, section, + encoding)) + + @classmethod + def from_source(cls, source, _warn=True): + """create a CryptPolicy instance from some source. + + this method autodetects the source type, and invokes + the appropriate constructor automatically. it attempts + to detect whether the source is a configuration string, a filepath, + a dictionary, or an existing CryptPolicy instance. + + .. deprecated:: 1.6 + + Create a new CryptContext, which could previously be done via + ``CryptContext(policy=CryptPolicy.from_source(source))``, should + now be done using an explicit method: the :class:`CryptContext` + constructor itself, :meth:`CryptContext.from_path`, + or :meth:`CryptContext.from_string`. + + Updating an existing CryptContext, which could previously be done via + ``context.policy = CryptPolicy.from_source(source)``, should + now be done using an explicit method: :meth:`CryptContext.update`, + or :meth:`CryptContext.load`. + """ + if _warn: + warn(_preamble + + "Instead of ``CryptPolicy.from_source()``, " + "use ``CryptContext.from_string(path)`` " + " or ``CryptContext.from_path(source)``, as appropriate.", + DeprecationWarning, stacklevel=2) + if isinstance(source, CryptPolicy): + return source + elif isinstance(source, dict): + return cls(_internal_context=CryptContext(**source)) + elif not isinstance(source, (bytes,unicode)): + raise TypeError("source must be CryptPolicy, dict, config string, " + "or file path: %r" % (type(source),)) + elif any(c in source for c in "\n\r\t") or not source.strip(" \t./;:"): + return cls(_internal_context=CryptContext.from_string(source)) + else: + return cls(_internal_context=CryptContext.from_path(source)) + + @classmethod + def from_sources(cls, sources, _warn=True): + """create a CryptPolicy instance by merging multiple sources. + + each source is interpreted as by :meth:`from_source`, + and the results are merged together. + + .. deprecated:: 1.6 + Instead of using this method to merge multiple policies together, + a :class:`CryptContext` instance should be created, and then + the multiple sources merged together via :meth:`CryptContext.load`. + """ + if _warn: + warn(_preamble + + "Instead of ``CryptPolicy.from_sources()``, " + "use the various CryptContext constructors " + " followed by ``context.update()``.", + DeprecationWarning, stacklevel=2) + if len(sources) == 0: + raise ValueError("no sources specified") + if len(sources) == 1: + return cls.from_source(sources[0], _warn=False) + kwds = {} + for source in sources: + kwds.update(cls.from_source(source, _warn=False)._context.to_dict(resolve=True)) + return cls(_internal_context=CryptContext(**kwds)) + + def replace(self, *args, **kwds): + """create a new CryptPolicy, optionally updating parts of the + existing configuration. + + .. deprecated:: 1.6 + Callers of this method should :meth:`CryptContext.update` or + :meth:`CryptContext.copy` instead. + """ + if self._stub_policy: + warn(_preamble + # pragma: no cover -- deprecated & unused + "Instead of ``context.policy.replace()``, " + "use ``context.update()`` or ``context.copy()``.", + DeprecationWarning, stacklevel=2) + else: + warn(_preamble + + "Instead of ``CryptPolicy().replace()``, " + "create a CryptContext instance and " + "use ``context.update()`` or ``context.copy()``.", + DeprecationWarning, stacklevel=2) + sources = [ self ] + if args: + sources.extend(args) + if kwds: + sources.append(kwds) + return CryptPolicy.from_sources(sources, _warn=False) + + #=================================================================== + # instance attrs + #=================================================================== + + # internal CryptContext we're wrapping to handle everything + # until this class is removed. + _context = None + + # flag indicating this is wrapper generated by the CryptContext.policy + # attribute, rather than one created independantly by the application. + _stub_policy = False + + #=================================================================== + # init + #=================================================================== + def __init__(self, *args, **kwds): + context = kwds.pop("_internal_context", None) + if context: + assert isinstance(context, CryptContext) + self._context = context + self._stub_policy = kwds.pop("_stub_policy", False) + assert not (args or kwds), "unexpected args: %r %r" % (args,kwds) + else: + if args: + if len(args) != 1: + raise TypeError("only one positional argument accepted") + if kwds: + raise TypeError("cannot specify positional arg and kwds") + kwds = args[0] + warn(_preamble + + "Instead of constructing a CryptPolicy instance, " + "create a CryptContext directly, or use ``context.update()`` " + "and ``context.load()`` to reconfigure existing CryptContext " + "instances.", + DeprecationWarning, stacklevel=2) + self._context = CryptContext(**kwds) + + #=================================================================== + # public interface for examining options + #=================================================================== + def has_schemes(self): + """return True if policy defines *any* schemes for use. + + .. deprecated:: 1.6 + applications should use ``bool(context.schemes())`` instead. + see :meth:`CryptContext.schemes`. + """ + if self._stub_policy: + warn(_preamble + # pragma: no cover -- deprecated & unused + "Instead of ``context.policy.has_schemes()``, " + "use ``bool(context.schemes())``.", + DeprecationWarning, stacklevel=2) + else: + warn(_preamble + + "Instead of ``CryptPolicy().has_schemes()``, " + "create a CryptContext instance and " + "use ``bool(context.schemes())``.", + DeprecationWarning, stacklevel=2) + return bool(self._context.schemes()) + + def iter_handlers(self): + """return iterator over handlers defined in policy. + + .. deprecated:: 1.6 + applications should use ``context.schemes(resolve=True))`` instead. + see :meth:`CryptContext.schemes`. + """ + if self._stub_policy: + warn(_preamble + + "Instead of ``context.policy.iter_handlers()``, " + "use ``context.schemes(resolve=True)``.", + DeprecationWarning, stacklevel=2) + else: + warn(_preamble + + "Instead of ``CryptPolicy().iter_handlers()``, " + "create a CryptContext instance and " + "use ``context.schemes(resolve=True)``.", + DeprecationWarning, stacklevel=2) + return self._context.schemes(resolve=True, unconfigured=True) + + def schemes(self, resolve=False): + """return list of schemes defined in policy. + + .. deprecated:: 1.6 + applications should use :meth:`CryptContext.schemes` instead. + """ + if self._stub_policy: + warn(_preamble + # pragma: no cover -- deprecated & unused + "Instead of ``context.policy.schemes()``, " + "use ``context.schemes()``.", + DeprecationWarning, stacklevel=2) + else: + warn(_preamble + + "Instead of ``CryptPolicy().schemes()``, " + "create a CryptContext instance and " + "use ``context.schemes()``.", + DeprecationWarning, stacklevel=2) + return list(self._context.schemes(resolve=resolve, unconfigured=True)) + + def get_handler(self, name=None, category=None, required=False): + """return handler as specified by name, or default handler. + + .. deprecated:: 1.6 + applications should use :meth:`CryptContext.handler` instead, + though note that the ``required`` keyword has been removed, + and the new method will always act as if ``required=True``. + """ + if self._stub_policy: + warn(_preamble + + "Instead of ``context.policy.get_handler()``, " + "use ``context.handler()``.", + DeprecationWarning, stacklevel=2) + else: + warn(_preamble + + "Instead of ``CryptPolicy().get_handler()``, " + "create a CryptContext instance and " + "use ``context.handler()``.", + DeprecationWarning, stacklevel=2) + # CryptContext.handler() doesn't support required=False, + # so wrapping it in try/except + try: + return self._context.handler(name, category, unconfigured=True) + except KeyError: + if required: + raise + else: + return None + + def get_min_verify_time(self, category=None): + """get min_verify_time setting for policy. + + .. deprecated:: 1.6 + min_verify_time option will be removed entirely in passlib 1.8 + + .. versionchanged:: 1.7 + this method now always returns the value automatically + calculated by :meth:`CryptContext.min_verify_time`, + any value specified by policy is ignored. + """ + warn("get_min_verify_time() and min_verify_time option is deprecated and ignored, " + "and will be removed in Passlib 1.8", DeprecationWarning, + stacklevel=2) + return 0 + + def get_options(self, name, category=None): + """return dictionary of options specific to a given handler. + + .. deprecated:: 1.6 + this method has no direct replacement in the 1.6 api, as there + is not a clearly defined use-case. however, examining the output of + :meth:`CryptContext.to_dict` should serve as the closest alternative. + """ + # XXX: might make a public replacement, but need more study of the use cases. + if self._stub_policy: + warn(_preamble + # pragma: no cover -- deprecated & unused + "``context.policy.get_options()`` will no longer be available.", + DeprecationWarning, stacklevel=2) + else: + warn(_preamble + + "``CryptPolicy().get_options()`` will no longer be available.", + DeprecationWarning, stacklevel=2) + if hasattr(name, "name"): + name = name.name + return self._context._config._get_record_options_with_flag(name, category)[0] + + def handler_is_deprecated(self, name, category=None): + """check if handler has been deprecated by policy. + + .. deprecated:: 1.6 + this method has no direct replacement in the 1.6 api, as there + is not a clearly defined use-case. however, examining the output of + :meth:`CryptContext.to_dict` should serve as the closest alternative. + """ + # XXX: might make a public replacement, but need more study of the use cases. + if self._stub_policy: + warn(_preamble + + "``context.policy.handler_is_deprecated()`` will no longer be available.", + DeprecationWarning, stacklevel=2) + else: + warn(_preamble + + "``CryptPolicy().handler_is_deprecated()`` will no longer be available.", + DeprecationWarning, stacklevel=2) + if hasattr(name, "name"): + name = name.name + return self._context.handler(name, category).deprecated + + #=================================================================== + # serialization + #=================================================================== + + def iter_config(self, ini=False, resolve=False): + """iterate over key/value pairs representing the policy object. + + .. deprecated:: 1.6 + applications should use :meth:`CryptContext.to_dict` instead. + """ + if self._stub_policy: + warn(_preamble + # pragma: no cover -- deprecated & unused + "Instead of ``context.policy.iter_config()``, " + "use ``context.to_dict().items()``.", + DeprecationWarning, stacklevel=2) + else: + warn(_preamble + + "Instead of ``CryptPolicy().iter_config()``, " + "create a CryptContext instance and " + "use ``context.to_dict().items()``.", + DeprecationWarning, stacklevel=2) + # hacked code that renders keys & values in manner that approximates + # old behavior. context.to_dict() is much cleaner. + context = self._context + if ini: + def render_key(key): + return context._render_config_key(key).replace("__", ".") + def render_value(value): + if isinstance(value, (list,tuple)): + value = ", ".join(value) + return value + resolve = False + else: + render_key = context._render_config_key + render_value = lambda value: value + return ( + (render_key(key), render_value(value)) + for key, value in context._config.iter_config(resolve) + ) + + def to_dict(self, resolve=False): + """export policy object as dictionary of options. + + .. deprecated:: 1.6 + applications should use :meth:`CryptContext.to_dict` instead. + """ + if self._stub_policy: + warn(_preamble + + "Instead of ``context.policy.to_dict()``, " + "use ``context.to_dict()``.", + DeprecationWarning, stacklevel=2) + else: + warn(_preamble + + "Instead of ``CryptPolicy().to_dict()``, " + "create a CryptContext instance and " + "use ``context.to_dict()``.", + DeprecationWarning, stacklevel=2) + return self._context.to_dict(resolve) + + def to_file(self, stream, section="passlib"): # pragma: no cover -- deprecated & unused + """export policy to file. + + .. deprecated:: 1.6 + applications should use :meth:`CryptContext.to_string` instead, + and then write the output to a file as desired. + """ + if self._stub_policy: + warn(_preamble + + "Instead of ``context.policy.to_file(stream)``, " + "use ``stream.write(context.to_string())``.", + DeprecationWarning, stacklevel=2) + else: + warn(_preamble + + "Instead of ``CryptPolicy().to_file(stream)``, " + "create a CryptContext instance and " + "use ``stream.write(context.to_string())``.", + DeprecationWarning, stacklevel=2) + out = self._context.to_string(section=section) + if PY2: + out = out.encode("utf-8") + stream.write(out) + + def to_string(self, section="passlib", encoding=None): + """export policy to file. + + .. deprecated:: 1.6 + applications should use :meth:`CryptContext.to_string` instead. + """ + if self._stub_policy: + warn(_preamble + # pragma: no cover -- deprecated & unused + "Instead of ``context.policy.to_string()``, " + "use ``context.to_string()``.", + DeprecationWarning, stacklevel=2) + else: + warn(_preamble + + "Instead of ``CryptPolicy().to_string()``, " + "create a CryptContext instance and " + "use ``context.to_string()``.", + DeprecationWarning, stacklevel=2) + out = self._context.to_string(section=section) + if encoding: + out = out.encode(encoding) + return out + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# _CryptConfig helper class +#============================================================================= +class _CryptConfig(object): + """parses, validates, and stores CryptContext config + + this is a helper used internally by CryptContext to handle + parsing, validation, and serialization of its config options. + split out from the main class, but not made public since + that just complicates interface too much (c.f. CryptPolicy) + + :arg source: config as dict mapping ``(cat,scheme,option) -> value`` + """ + #=================================================================== + # instance attrs + #=================================================================== + + # triple-nested dict which maps scheme -> category -> key -> value, + # storing all hash-specific options + _scheme_options = None + + # double-nested dict which maps key -> category -> value + # storing all CryptContext options + _context_options = None + + # tuple of handler objects + handlers = None + + # tuple of scheme objects in same order as handlers + schemes = None + + # tuple of categories in alphabetical order (not including None) + categories = None + + # set of all context keywords used by active schemes + context_kwds = None + + # dict mapping category -> default scheme + _default_schemes = None + + # dict mapping (scheme, category) -> custom handler + _records = None + + # dict mapping category -> list of custom handler instances for that category, + # in order of schemes(). populated on demand by _get_record_list() + _record_lists = None + + #=================================================================== + # constructor + #=================================================================== + def __init__(self, source): + self._init_scheme_list(source.get((None,None,"schemes"))) + self._init_options(source) + self._init_default_schemes() + self._init_records() + + def _init_scheme_list(self, data): + """initialize .handlers and .schemes attributes""" + handlers = [] + schemes = [] + if isinstance(data, native_string_types): + data = splitcomma(data) + for elem in data or (): + # resolve elem -> handler & scheme + if hasattr(elem, "name"): + handler = elem + scheme = handler.name + _validate_handler_name(scheme) + elif isinstance(elem, native_string_types): + handler = get_crypt_handler(elem) + scheme = handler.name + else: + raise TypeError("scheme must be name or CryptHandler, " + "not %r" % type(elem)) + + # check scheme name isn't already in use + if scheme in schemes: + raise KeyError("multiple handlers with same name: %r" % + (scheme,)) + + # add to handler list + handlers.append(handler) + schemes.append(scheme) + + self.handlers = tuple(handlers) + self.schemes = tuple(schemes) + + #=================================================================== + # lowlevel options + #=================================================================== + + #--------------------------------------------------------------- + # init lowlevel option storage + #--------------------------------------------------------------- + def _init_options(self, source): + """load config dict into internal representation, + and init .categories attr + """ + # prepare dicts & locals + norm_scheme_option = self._norm_scheme_option + norm_context_option = self._norm_context_option + self._scheme_options = scheme_options = {} + self._context_options = context_options = {} + categories = set() + + # load source config into internal storage + for (cat, scheme, key), value in iteritems(source): + categories.add(cat) + explicit_scheme = scheme + if not cat and not scheme and key in _global_settings: + # going forward, not using "__all__" format. instead... + # whitelisting set of keys which should be passed to (all) schemes, + # rather than passed to the CryptContext itself + scheme = "all" + if scheme: + # normalize scheme option + key, value = norm_scheme_option(key, value) + + # e.g. things like "min_rounds" should never be set cross-scheme + # this will be fatal under 2.0. + if scheme == "all" and key not in _global_settings: + warn("The '%s' option should be configured per-algorithm, and not set " + "globally in the context; This will be an error in Passlib 2.0" % + (key,), PasslibConfigWarning) + + # this scheme is going away in 2.0; + # but most keys deserve an extra warning since it impacts security. + if explicit_scheme == "all": + warn("The 'all' scheme is deprecated as of Passlib 1.7, " + "and will be removed in Passlib 2.0; Please configure " + "options on a per-algorithm basis.", DeprecationWarning) + + # store in scheme_options + # map structure: scheme_options[scheme][category][key] = value + try: + category_map = scheme_options[scheme] + except KeyError: + scheme_options[scheme] = {cat: {key: value}} + else: + try: + option_map = category_map[cat] + except KeyError: + category_map[cat] = {key: value} + else: + option_map[key] = value + else: + # normalize context option + if cat and key == "schemes": + raise KeyError("'schemes' context option is not allowed " + "per category") + key, value = norm_context_option(cat, key, value) + if key == "min_verify_time": # ignored in 1.7, to be removed in 1.8 + continue + + # store in context_options + # map structure: context_options[key][category] = value + try: + category_map = context_options[key] + except KeyError: + context_options[key] = {cat: value} + else: + category_map[cat] = value + + # store list of configured categories + categories.discard(None) + self.categories = tuple(sorted(categories)) + + def _norm_scheme_option(self, key, value): + # check for invalid options + if key in _forbidden_scheme_options: + raise KeyError("%r option not allowed in CryptContext " + "configuration" % (key,)) + # coerce strings for certain fields (e.g. min_rounds uses ints) + if isinstance(value, native_string_types): + func = _coerce_scheme_options.get(key) + if func: + value = func(value) + return key, value + + def _norm_context_option(self, cat, key, value): + schemes = self.schemes + if key == "default": + if hasattr(value, "name"): + value = value.name + elif not isinstance(value, native_string_types): + raise ExpectedTypeError(value, "str", "default") + if schemes and value not in schemes: + raise KeyError("default scheme not found in policy") + elif key == "deprecated": + if isinstance(value, native_string_types): + value = splitcomma(value) + elif not isinstance(value, (list,tuple)): + raise ExpectedTypeError(value, "str or seq", "deprecated") + if 'auto' in value: + # XXX: have any statements been made about when this is default? + # should do it in 1.8 at latest. + if len(value) > 1: + raise ValueError("cannot list other schemes if " + "``deprecated=['auto']`` is used") + elif schemes: + # make sure list of deprecated schemes is subset of configured schemes + for scheme in value: + if not isinstance(scheme, native_string_types): + raise ExpectedTypeError(value, "str", "deprecated element") + if scheme not in schemes: + raise KeyError("deprecated scheme not found " + "in policy: %r" % (scheme,)) + elif key == "min_verify_time": + warn("'min_verify_time' was deprecated in Passlib 1.6, is " + "ignored in 1.7, and will be removed in 1.8", + DeprecationWarning) + elif key == "harden_verify": + warn("'harden_verify' is deprecated & ignored as of Passlib 1.7.1, " + " and will be removed in 1.8", + DeprecationWarning) + elif key != "schemes": + raise KeyError("unknown CryptContext keyword: %r" % (key,)) + return key, value + + #--------------------------------------------------------------- + # reading context options + #--------------------------------------------------------------- + def get_context_optionmap(self, key, _default={}): + """return dict mapping category->value for specific context option. + + .. warning:: treat return value as readonly! + """ + return self._context_options.get(key, _default) + + def get_context_option_with_flag(self, category, key): + """return value of specific option, handling category inheritance. + also returns flag indicating whether value is category-specific. + """ + try: + category_map = self._context_options[key] + except KeyError: + return None, False + value = category_map.get(None) + if category: + try: + alt = category_map[category] + except KeyError: + pass + else: + if value is None or alt != value: + return alt, True + return value, False + + #--------------------------------------------------------------- + # reading scheme options + #--------------------------------------------------------------- + def _get_scheme_optionmap(self, scheme, category, default={}): + """return all options for (scheme,category) combination + + .. warning:: treat return value as readonly! + """ + try: + return self._scheme_options[scheme][category] + except KeyError: + return default + + def get_base_handler(self, scheme): + return self.handlers[self.schemes.index(scheme)] + + @staticmethod + def expand_settings(handler): + setting_kwds = handler.setting_kwds + if 'rounds' in handler.setting_kwds: + # XXX: historically this extras won't be listed in setting_kwds + setting_kwds += uh.HasRounds.using_rounds_kwds + return setting_kwds + + # NOTE: this is only used by _get_record_options_with_flag()... + def get_scheme_options_with_flag(self, scheme, category): + """return composite dict of all options set for scheme. + includes options inherited from 'all' and from default category. + result can be modified. + returns (kwds, has_cat_specific_options) + """ + # start out with copy of global options + get_optionmap = self._get_scheme_optionmap + kwds = get_optionmap("all", None).copy() + has_cat_options = False + + # add in category-specific global options + if category: + defkwds = kwds.copy() # <-- used to detect category-specific options + kwds.update(get_optionmap("all", category)) + + # filter out global settings not supported by handler + allowed_settings = self.expand_settings(self.get_base_handler(scheme)) + for key in set(kwds).difference(allowed_settings): + kwds.pop(key) + if category: + for key in set(defkwds).difference(allowed_settings): + defkwds.pop(key) + + # add in default options for scheme + other = get_optionmap(scheme, None) + kwds.update(other) + + # load category-specific options for scheme + if category: + defkwds.update(other) + kwds.update(get_optionmap(scheme, category)) + + # compare default category options to see if there's anything + # category-specific + if kwds != defkwds: + has_cat_options = True + + return kwds, has_cat_options + + #=================================================================== + # deprecated & default schemes + #=================================================================== + def _init_default_schemes(self): + """initialize maps containing default scheme for each category. + + have to do this after _init_options(), since the default scheme + is affected by the list of deprecated schemes. + """ + # init maps & locals + get_optionmap = self.get_context_optionmap + default_map = self._default_schemes = get_optionmap("default").copy() + dep_map = get_optionmap("deprecated") + schemes = self.schemes + if not schemes: + return + + # figure out default scheme + deps = dep_map.get(None) or () + default = default_map.get(None) + if not default: + for scheme in schemes: + if scheme not in deps: + default_map[None] = scheme + break + else: + raise ValueError("must have at least one non-deprecated scheme") + elif default in deps: + raise ValueError("default scheme cannot be deprecated") + + # figure out per-category default schemes, + for cat in self.categories: + cdeps = dep_map.get(cat, deps) + cdefault = default_map.get(cat, default) + if not cdefault: + for scheme in schemes: + if scheme not in cdeps: + default_map[cat] = scheme + break + else: + raise ValueError("must have at least one non-deprecated " + "scheme for %r category" % cat) + elif cdefault in cdeps: + raise ValueError("default scheme for %r category " + "cannot be deprecated" % cat) + + def default_scheme(self, category): + """return default scheme for specific category""" + defaults = self._default_schemes + try: + return defaults[category] + except KeyError: + pass + if not self.schemes: + raise KeyError("no hash schemes configured for this " + "CryptContext instance") + return defaults[None] + + def is_deprecated_with_flag(self, scheme, category): + """is scheme deprecated under particular category?""" + depmap = self.get_context_optionmap("deprecated") + def test(cat): + source = depmap.get(cat, depmap.get(None)) + if source is None: + return None + elif 'auto' in source: + return scheme != self.default_scheme(cat) + else: + return scheme in source + value = test(None) or False + if category: + alt = test(category) + if alt is not None and value != alt: + return alt, True + return value, False + + #=================================================================== + # CryptRecord objects + #=================================================================== + def _init_records(self): + # NOTE: this step handles final validation of settings, + # checking for violations against handler's internal invariants. + # this is why we create all the records now, + # so CryptContext throws error immediately rather than later. + self._record_lists = {} + records = self._records = {} + all_context_kwds = self.context_kwds = set() + get_options = self._get_record_options_with_flag + categories = (None,) + self.categories + for handler in self.handlers: + scheme = handler.name + all_context_kwds.update(handler.context_kwds) + for cat in categories: + kwds, has_cat_options = get_options(scheme, cat) + if cat is None or has_cat_options: + records[scheme, cat] = self._create_record(handler, cat, **kwds) + # NOTE: if handler has no category-specific opts, get_record() + # will automatically use the default category's record. + # NOTE: default records for specific category stored under the + # key (None,category); these are populated on-demand by get_record(). + + @staticmethod + def _create_record(handler, category=None, deprecated=False, **settings): + # create custom handler if needed. + try: + # XXX: relaxed=True is mostly here to retain backwards-compat behavior. + # could make this optional flag in future. + subcls = handler.using(relaxed=True, **settings) + except TypeError as err: + m = re.match(r".* unexpected keyword argument '(.*)'$", str(err)) + if m and m.group(1) in settings: + # translate into KeyError, for backwards compat. + # XXX: push this down to GenericHandler.using() implementation? + key = m.group(1) + raise KeyError("keyword not supported by %s handler: %r" % + (handler.name, key)) + raise + + # using private attrs to store some extra metadata in custom handler + assert subcls is not handler, "expected unique variant of handler" + ##subcls._Context__category = category + subcls._Context__orig_handler = handler + subcls.deprecated = deprecated # attr reserved for this purpose + return subcls + + def _get_record_options_with_flag(self, scheme, category): + """return composite dict of options for given scheme + category. + + this is currently a private method, though some variant + of its output may eventually be made public. + + given a scheme & category, it returns two things: + a set of all the keyword options to pass to :meth:`_create_record`, + and a bool flag indicating whether any of these options + were specific to the named category. if this flag is false, + the options are identical to the options for the default category. + + the options dict includes all the scheme-specific settings, + as well as optional *deprecated* keyword. + """ + # get scheme options + kwds, has_cat_options = self.get_scheme_options_with_flag(scheme, category) + + # throw in deprecated flag + value, not_inherited = self.is_deprecated_with_flag(scheme, category) + if value: + kwds['deprecated'] = True + if not_inherited: + has_cat_options = True + + return kwds, has_cat_options + + def get_record(self, scheme, category): + """return record for specific scheme & category (cached)""" + # NOTE: this is part of the critical path shared by + # all of CryptContext's PasswordHash methods, + # hence all the caching and error checking. + + # quick lookup in cache + try: + return self._records[scheme, category] + except KeyError: + pass + + # type check + if category is not None and not isinstance(category, native_string_types): + if PY2 and isinstance(category, unicode): + # for compatibility with unicode-centric py2 apps + return self.get_record(scheme, category.encode("utf-8")) + raise ExpectedTypeError(category, "str or None", "category") + if scheme is not None and not isinstance(scheme, native_string_types): + raise ExpectedTypeError(scheme, "str or None", "scheme") + + # if scheme=None, + # use record for category's default scheme, and cache result. + if not scheme: + default = self.default_scheme(category) + assert default + record = self._records[None, category] = self.get_record(default, + category) + return record + + # if no record for (scheme, category), + # use record for (scheme, None), and cache result. + if category: + try: + cache = self._records + record = cache[scheme, category] = cache[scheme, None] + return record + except KeyError: + pass + + # scheme not found in configuration for default category + raise KeyError("crypt algorithm not found in policy: %r" % (scheme,)) + + def _get_record_list(self, category=None): + """return list of records for category (cached) + + this is an internal helper used only by identify_record() + """ + # type check of category - handled by _get_record() + # quick lookup in cache + try: + return self._record_lists[category] + except KeyError: + pass + # cache miss - build list from scratch + value = self._record_lists[category] = [ + self.get_record(scheme, category) + for scheme in self.schemes + ] + return value + + def identify_record(self, hash, category, required=True): + """internal helper to identify appropriate custom handler for hash""" + # NOTE: this is part of the critical path shared by + # all of CryptContext's PasswordHash methods, + # hence all the caching and error checking. + # FIXME: if multiple hashes could match (e.g. lmhash vs nthash) + # this will only return first match. might want to do something + # about this in future, but for now only hashes with + # unique identifiers will work properly in a CryptContext. + # XXX: if all handlers have a unique prefix (e.g. all are MCF / LDAP), + # could use dict-lookup to speed up this search. + if not isinstance(hash, unicode_or_bytes_types): + raise ExpectedStringError(hash, "hash") + # type check of category - handled by _get_record_list() + for record in self._get_record_list(category): + if record.identify(hash): + return record + if not required: + return None + elif not self.schemes: + raise KeyError("no crypt algorithms supported") + else: + raise exc.UnknownHashError("hash could not be identified") + + @memoized_property + def disabled_record(self): + for record in self._get_record_list(None): + if record.is_disabled: + return record + raise RuntimeError("no disabled hasher present " + "(perhaps add 'unix_disabled' to list of schemes?)") + + #=================================================================== + # serialization + #=================================================================== + def iter_config(self, resolve=False): + """regenerate original config. + + this is an iterator which yields ``(cat,scheme,option),value`` items, + in the order they generally appear inside an INI file. + if interpreted as a dictionary, it should match the original + keywords passed to the CryptContext (aside from any canonization). + + it's mainly used as the internal backend for most of the public + serialization methods. + """ + # grab various bits of data + scheme_options = self._scheme_options + context_options = self._context_options + scheme_keys = sorted(scheme_options) + context_keys = sorted(context_options) + + # write loaded schemes (may differ from 'schemes' local var) + if 'schemes' in context_keys: + context_keys.remove("schemes") + value = self.handlers if resolve else self.schemes + if value: + yield (None, None, "schemes"), list(value) + + # then run through config for each user category + for cat in (None,) + self.categories: + + # write context options + for key in context_keys: + try: + value = context_options[key][cat] + except KeyError: + pass + else: + if isinstance(value, list): + value = list(value) + yield (cat, None, key), value + + # write per-scheme options for all schemes. + for scheme in scheme_keys: + try: + kwds = scheme_options[scheme][cat] + except KeyError: + pass + else: + for key in sorted(kwds): + yield (cat, scheme, key), kwds[key] + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# main CryptContext class +#============================================================================= +class CryptContext(object): + """Helper for hashing & verifying passwords using multiple algorithms. + + Instances of this class allow applications to choose a specific + set of hash algorithms which they wish to support, set limits and defaults + for the rounds and salt sizes those algorithms should use, flag + which algorithms should be deprecated, and automatically handle + migrating users to stronger hashes when they log in. + + Basic usage:: + + >>> ctx = CryptContext(schemes=[...]) + + See the Passlib online documentation for details and full documentation. + """ + # FIXME: altering the configuration of this object isn't threadsafe, + # but is generally only done during application init, so not a major + # issue (just yet). + + # XXX: would like some way to restrict the categories that are allowed, + # to restrict what the app OR the config can use. + + # XXX: add wrap/unwrap callback hooks so app can mutate hash format? + + # XXX: add method for detecting and warning user about schemes + # which don't have any good distinguishing marks? + # or greedy ones (unix_disabled, plaintext) which are not listed at the end? + + #=================================================================== + # instance attrs + #=================================================================== + + # _CryptConfig instance holding current parsed config + _config = None + + # copy of _config methods, stored in CryptContext instance for speed. + _get_record = None + _identify_record = None + + #=================================================================== + # secondary constructors + #=================================================================== + @classmethod + def _norm_source(cls, source): + """internal helper - accepts string, dict, or context""" + if isinstance(source, dict): + return cls(**source) + elif isinstance(source, cls): + return source + else: + self = cls() + self.load(source) + return self + + @classmethod + def from_string(cls, source, section="passlib", encoding="utf-8"): + """create new CryptContext instance from an INI-formatted string. + + :type source: unicode or bytes + :arg source: + string containing INI-formatted content. + + :type section: str + :param section: + option name of section to read from, defaults to ``"passlib"``. + + :type encoding: str + :arg encoding: + optional encoding used when source is bytes, defaults to ``"utf-8"``. + + :returns: + new :class:`CryptContext` instance, configured based on the + parameters in the *source* string. + + Usage example:: + + >>> from passlib.context import CryptContext + >>> context = CryptContext.from_string(''' + ... [passlib] + ... schemes = sha256_crypt, des_crypt + ... sha256_crypt__default_rounds = 30000 + ... ''') + + .. versionadded:: 1.6 + + .. seealso:: :meth:`to_string`, the inverse of this constructor. + """ + if not isinstance(source, unicode_or_bytes_types): + raise ExpectedTypeError(source, "unicode or bytes", "source") + self = cls(_autoload=False) + self.load(source, section=section, encoding=encoding) + return self + + @classmethod + def from_path(cls, path, section="passlib", encoding="utf-8"): + """create new CryptContext instance from an INI-formatted file. + + this functions exactly the same as :meth:`from_string`, + except that it loads from a local file. + + :type path: str + :arg path: + path to local file containing INI-formatted config. + + :type section: str + :param section: + option name of section to read from, defaults to ``"passlib"``. + + :type encoding: str + :arg encoding: + encoding used to load file, defaults to ``"utf-8"``. + + :returns: + new CryptContext instance, configured based on the parameters + stored in the file *path*. + + .. versionadded:: 1.6 + + .. seealso:: :meth:`from_string` for an equivalent usage example. + """ + self = cls(_autoload=False) + self.load_path(path, section=section, encoding=encoding) + return self + + def copy(self, **kwds): + """Return copy of existing CryptContext instance. + + This function returns a new CryptContext instance whose configuration + is exactly the same as the original, with the exception that any keywords + passed in will take precedence over the original settings. + As an example:: + + >>> from passlib.context import CryptContext + + >>> # given an existing context... + >>> ctx1 = CryptContext(["sha256_crypt", "md5_crypt"]) + + >>> # copy can be used to make a clone, and update + >>> # some of the settings at the same time... + >>> ctx2 = custom_app_context.copy(default="md5_crypt") + + >>> # and the original will be unaffected by the change + >>> ctx1.default_scheme() + "sha256_crypt" + >>> ctx2.default_scheme() + "md5_crypt" + + .. versionadded:: 1.6 + This method was previously named :meth:`!replace`. That alias + has been deprecated, and will be removed in Passlib 1.8. + + .. seealso:: :meth:`update` + """ + # XXX: it would be faster to store ref to self._config, + # but don't want to share config objects til sure + # can rely on them being immutable. + other = CryptContext(_autoload=False) + other.load(self) + if kwds: + other.load(kwds, update=True) + return other + + def using(self, **kwds): + """ + alias for :meth:`copy`, to match PasswordHash.using() + """ + return self.copy(**kwds) + + def replace(self, **kwds): + """deprecated alias of :meth:`copy`""" + warn("CryptContext().replace() has been deprecated in Passlib 1.6, " + "and will be removed in Passlib 1.8, " + "it has been renamed to CryptContext().copy()", + DeprecationWarning, stacklevel=2) + return self.copy(**kwds) + + #=================================================================== + # init + #=================================================================== + def __init__(self, schemes=None, + # keyword only... + policy=_UNSET, # <-- deprecated + _autoload=True, **kwds): + # XXX: add ability to make flag certain contexts as immutable, + # e.g. the builtin passlib ones? + # XXX: add a name or import path for the contexts, to help out repr? + if schemes is not None: + kwds['schemes'] = schemes + if policy is not _UNSET: + warn("The CryptContext ``policy`` keyword has been deprecated as of Passlib 1.6, " + "and will be removed in Passlib 1.8; please use " + "``CryptContext.from_string()` or " + "``CryptContext.from_path()`` instead.", + DeprecationWarning) + if policy is None: + self.load(kwds) + elif isinstance(policy, CryptPolicy): + self.load(policy._context) + self.update(kwds) + else: + raise TypeError("policy must be a CryptPolicy instance") + elif _autoload: + self.load(kwds) + else: + assert not kwds, "_autoload=False and kwds are mutually exclusive" + + # XXX: would this be useful? + ##def __str__(self): + ## if PY3: + ## return self.to_string() + ## else: + ## return self.to_string().encode("utf-8") + + def __repr__(self): + return "" % id(self) + + #=================================================================== + # deprecated policy object + #=================================================================== + def _get_policy(self): + # The CryptPolicy class has been deprecated, so to support any + # legacy accesses, we create a stub policy object so .policy attr + # will continue to work. + # + # the code waits until app accesses a specific policy object attribute + # before issuing deprecation warning, so developer gets method-specific + # suggestion for how to upgrade. + + # NOTE: making a copy of the context so the policy acts like a snapshot, + # to retain the pre-1.6 behavior. + return CryptPolicy(_internal_context=self.copy(), _stub_policy=True) + + def _set_policy(self, policy): + warn("The CryptPolicy class and the ``context.policy`` attribute have " + "been deprecated as of Passlib 1.6, and will be removed in " + "Passlib 1.8; please use the ``context.load()`` and " + "``context.update()`` methods instead.", + DeprecationWarning, stacklevel=2) + if isinstance(policy, CryptPolicy): + self.load(policy._context) + else: + raise TypeError("expected CryptPolicy instance") + + policy = property(_get_policy, _set_policy, + doc="[deprecated] returns CryptPolicy instance " + "tied to this CryptContext") + + #=================================================================== + # loading / updating configuration + #=================================================================== + @staticmethod + def _parse_ini_stream(stream, section, filename): + """helper read INI from stream, extract passlib section as dict""" + # NOTE: this expects a unicode stream under py3, + # and a utf-8 bytes stream under py2, + # allowing the resulting dict to always use native strings. + p = SafeConfigParser() + if PY3: + # python 3.2 deprecated readfp in favor of read_file + p.read_file(stream, filename) + else: + p.readfp(stream, filename) + # XXX: could change load() to accept list of items, + # and skip intermediate dict creation + return dict(p.items(section)) + + def load_path(self, path, update=False, section="passlib", encoding="utf-8"): + """Load new configuration into CryptContext from a local file. + + This function is a wrapper for :meth:`load` which + loads a configuration string from the local file *path*, + instead of an in-memory source. Its behavior and options + are otherwise identical to :meth:`!load` when provided with + an INI-formatted string. + + .. versionadded:: 1.6 + """ + def helper(stream): + kwds = self._parse_ini_stream(stream, section, path) + return self.load(kwds, update=update) + if PY3: + # decode to unicode, which load() expected under py3 + with open(path, "rt", encoding=encoding) as stream: + return helper(stream) + elif encoding in ["utf-8", "ascii"]: + # keep as utf-8 bytes, which load() expects under py2 + with open(path, "rb") as stream: + return helper(stream) + else: + # transcode to utf-8 bytes + with open(path, "rb") as fh: + tmp = fh.read().decode(encoding).encode("utf-8") + return helper(BytesIO(tmp)) + + def load(self, source, update=False, section="passlib", encoding="utf-8"): + """Load new configuration into CryptContext, replacing existing config. + + :arg source: + source of new configuration to load. + this value can be a number of different types: + + * a :class:`!dict` object, or compatible Mapping + + the key/value pairs will be interpreted the same + keywords for the :class:`CryptContext` class constructor. + + * a :class:`!unicode` or :class:`!bytes` string + + this will be interpreted as an INI-formatted file, + and appropriate key/value pairs will be loaded from + the specified *section*. + + * another :class:`!CryptContext` object. + + this will export a snapshot of its configuration + using :meth:`to_dict`. + + :type update: bool + :param update: + By default, :meth:`load` will replace the existing configuration + entirely. If ``update=True``, it will preserve any existing + configuration options that are not overridden by the new source, + much like the :meth:`update` method. + + :type section: str + :param section: + When parsing an INI-formatted string, :meth:`load` will look for + a section named ``"passlib"``. This option allows an alternate + section name to be used. Ignored when loading from a dictionary. + + :type encoding: str + :param encoding: + Encoding to use when **source** is bytes. + Defaults to ``"utf-8"``. Ignored when loading from a dictionary. + + .. deprecated:: 1.8 + + This keyword, and support for bytes input, will be dropped in Passlib 2.0 + + :raises TypeError: + * If the source cannot be identified. + * If an unknown / malformed keyword is encountered. + + :raises ValueError: + If an invalid keyword value is encountered. + + .. note:: + + If an error occurs during a :meth:`!load` call, the :class:`!CryptContext` + instance will be restored to the configuration it was in before + the :meth:`!load` call was made; this is to ensure it is + *never* left in an inconsistent state due to a load error. + + .. versionadded:: 1.6 + """ + #----------------------------------------------------------- + # autodetect source type, convert to dict + #----------------------------------------------------------- + parse_keys = True + if isinstance(source, unicode_or_bytes_types): + if PY3: + source = to_unicode(source, encoding, param="source") + else: + source = to_bytes(source, "utf-8", source_encoding=encoding, + param="source") + source = self._parse_ini_stream(NativeStringIO(source), section, + "") + elif isinstance(source, CryptContext): + # extract dict directly from config, so it can be merged later + source = dict(source._config.iter_config(resolve=True)) + parse_keys = False + elif not hasattr(source, "items"): + # mappings are left alone, otherwise throw an error. + raise ExpectedTypeError(source, "string or dict", "source") + + # XXX: add support for other iterable types, e.g. sequence of pairs? + + #----------------------------------------------------------- + # parse dict keys into (category, scheme, option) format, + # and merge with existing configuration if needed. + #----------------------------------------------------------- + if parse_keys: + parse = self._parse_config_key + source = dict((parse(key), value) + for key, value in iteritems(source)) + if update and self._config is not None: + # if updating, do nothing if source is empty, + if not source: + return + # otherwise overlay source on top of existing config + tmp = source + source = dict(self._config.iter_config(resolve=True)) + source.update(tmp) + + #----------------------------------------------------------- + # compile into _CryptConfig instance, and update state + #----------------------------------------------------------- + config = _CryptConfig(source) + self._config = config + self._reset_dummy_verify() + self._get_record = config.get_record + self._identify_record = config.identify_record + if config.context_kwds: + # (re-)enable method for this instance (in case ELSE clause below ran last load). + self.__dict__.pop("_strip_unused_context_kwds", None) + else: + # disable method for this instance, it's not needed. + self._strip_unused_context_kwds = None + + @staticmethod + def _parse_config_key(ckey): + """helper used to parse ``cat__scheme__option`` keys into a tuple""" + # split string into 1-3 parts + assert isinstance(ckey, native_string_types) + parts = ckey.replace(".", "__").split("__") + count = len(parts) + if count == 1: + cat, scheme, key = None, None, parts[0] + elif count == 2: + cat = None + scheme, key = parts + elif count == 3: + cat, scheme, key = parts + else: + raise TypeError("keys must have less than 3 separators: %r" % + (ckey,)) + # validate & normalize the parts + if cat == "default": + cat = None + elif not cat and cat is not None: + raise TypeError("empty category: %r" % ckey) + if scheme == "context": + scheme = None + elif not scheme and scheme is not None: + raise TypeError("empty scheme: %r" % ckey) + if not key: + raise TypeError("empty option: %r" % ckey) + return cat, scheme, key + + def update(self, *args, **kwds): + """Helper for quickly changing configuration. + + This acts much like the :meth:`!dict.update` method: + it updates the context's configuration, + replacing the original value(s) for the specified keys, + and preserving the rest. + It accepts any :ref:`keyword ` + accepted by the :class:`!CryptContext` constructor. + + .. versionadded:: 1.6 + + .. seealso:: :meth:`copy` + """ + if args: + if len(args) > 1: + raise TypeError("expected at most one positional argument") + if kwds: + raise TypeError("positional arg and keywords mutually exclusive") + self.load(args[0], update=True) + elif kwds: + self.load(kwds, update=True) + + # XXX: make this public? even just as flag to load? + # FIXME: this function suffered some bitrot in 1.6.1, + # will need to be updated before works again. + ##def _simplify(self): + ## "helper to remove redundant/unused options" + ## # don't do anything if no schemes are defined + ## if not self._schemes: + ## return + ## + ## def strip_items(target, filter): + ## keys = [key for key,value in iteritems(target) + ## if filter(key,value)] + ## for key in keys: + ## del target[key] + ## + ## # remove redundant default. + ## defaults = self._default_schemes + ## if defaults.get(None) == self._schemes[0]: + ## del defaults[None] + ## + ## # remove options for unused schemes. + ## scheme_options = self._scheme_options + ## schemes = self._schemes + ("all",) + ## strip_items(scheme_options, lambda k,v: k not in schemes) + ## + ## # remove rendundant cat defaults. + ## cur = self.default_scheme() + ## strip_items(defaults, lambda k,v: k and v==cur) + ## + ## # remove redundant category deprecations. + ## # TODO: this should work w/ 'auto', but needs closer inspection + ## deprecated = self._deprecated_schemes + ## cur = self._deprecated_schemes.get(None) + ## strip_items(deprecated, lambda k,v: k and v==cur) + ## + ## # remove redundant category options. + ## for scheme, config in iteritems(scheme_options): + ## if None in config: + ## cur = config[None] + ## strip_items(config, lambda k,v: k and v==cur) + ## + ## # XXX: anything else? + + #=================================================================== + # reading configuration + #=================================================================== + def schemes(self, resolve=False, category=None, unconfigured=False): + """return schemes loaded into this CryptContext instance. + + :type resolve: bool + :arg resolve: + if ``True``, will return a tuple of :class:`~passlib.ifc.PasswordHash` + objects instead of their names. + + :returns: + returns tuple of the schemes configured for this context + via the *schemes* option. + + .. versionadded:: 1.6 + This was previously available as ``CryptContext().policy.schemes()`` + + .. seealso:: the :ref:`schemes ` option for usage example. + """ + # XXX: should resolv return records rather than handlers? + # or deprecate resolve keyword completely? + # offering up a .hashers Mapping in v1.8 would be great. + # NOTE: supporting 'category' and 'unconfigured' kwds as of 1.7 + # just to pass through to .handler(), but not documenting them... + # may not need to put them to use. + schemes = self._config.schemes + if resolve: + return tuple(self.handler(scheme, category, unconfigured=unconfigured) + for scheme in schemes) + else: + return schemes + + def default_scheme(self, category=None, resolve=False, unconfigured=False): + """return name of scheme that :meth:`hash` will use by default. + + :type resolve: bool + :arg resolve: + if ``True``, will return a :class:`~passlib.ifc.PasswordHash` + object instead of the name. + + :type category: str or None + :param category: + Optional :ref:`user category `. + If specified, this will return the catgory-specific default scheme instead. + + :returns: + name of the default scheme. + + .. seealso:: the :ref:`default ` option for usage example. + + .. versionadded:: 1.6 + + .. versionchanged:: 1.7 + + This now returns a hasher configured with any CryptContext-specific + options (custom rounds settings, etc). Previously this returned + the base hasher from :mod:`passlib.hash`. + """ + # XXX: deprecate this in favor of .handler() or whatever it's replaced with? + # NOTE: supporting 'unconfigured' kwds as of 1.7 + # just to pass through to .handler(), but not documenting them... + # may not need to put them to use. + hasher = self.handler(None, category, unconfigured=unconfigured) + return hasher if resolve else hasher.name + + # XXX: need to decide if exposing this would be useful in any way + ##def categories(self): + ## """return user-categories with algorithm-specific options in this CryptContext. + ## + ## this will always return a tuple. + ## if no categories besides the default category have been configured, + ## the tuple will be empty. + ## """ + ## return self._config.categories + + # XXX: need to decide if exposing this would be useful to applications + # in any meaningful way that isn't already served by to_dict() + ##def options(self, scheme, category=None): + ## kwds, percat = self._config.get_options(scheme, category) + ## return kwds + + def handler(self, scheme=None, category=None, unconfigured=False): + """helper to resolve name of scheme -> :class:`~passlib.ifc.PasswordHash` object used by scheme. + + :arg scheme: + This should identify the scheme to lookup. + If omitted or set to ``None``, this will return the handler + for the default scheme. + + :arg category: + If a user category is specified, and no scheme is provided, + it will use the default for that category. + Otherwise this parameter is ignored. + + :param unconfigured: + + By default, this returns a handler object whose .hash() + and .needs_update() methods will honor the configured + provided by CryptContext. See ``unconfigured=True`` + to get the underlying handler from before any context-specific + configuration was applied. + + :raises KeyError: + If the scheme does not exist OR is not being used within this context. + + :returns: + :class:`~passlib.ifc.PasswordHash` object used to implement + the named scheme within this context (this will usually + be one of the objects from :mod:`passlib.hash`) + + .. versionadded:: 1.6 + This was previously available as ``CryptContext().policy.get_handler()`` + + .. versionchanged:: 1.7 + + This now returns a hasher configured with any CryptContext-specific + options (custom rounds settings, etc). Previously this returned + the base hasher from :mod:`passlib.hash`. + """ + try: + hasher = self._get_record(scheme, category) + if unconfigured: + return hasher._Context__orig_handler + else: + return hasher + except KeyError: + pass + if self._config.handlers: + raise KeyError("crypt algorithm not found in this " + "CryptContext instance: %r" % (scheme,)) + else: + raise KeyError("no crypt algorithms loaded in this " + "CryptContext instance") + + def _get_unregistered_handlers(self): + """check if any handlers in this context aren't in the global registry""" + return tuple(handler for handler in self._config.handlers + if not _is_handler_registered(handler)) + + @property + def context_kwds(self): + """ + return :class:`!set` containing union of all :ref:`contextual keywords ` + supported by the handlers in this context. + + .. versionadded:: 1.6.6 + """ + return self._config.context_kwds + + #=================================================================== + # exporting config + #=================================================================== + @staticmethod + def _render_config_key(key): + """convert 3-part config key to single string""" + cat, scheme, option = key + if cat: + return "%s__%s__%s" % (cat, scheme or "context", option) + elif scheme: + return "%s__%s" % (scheme, option) + else: + return option + + @staticmethod + def _render_ini_value(key, value): + """render value to string suitable for INI file""" + # convert lists to comma separated lists + # (mainly 'schemes' & 'deprecated') + if isinstance(value, (list,tuple)): + value = ", ".join(value) + + # convert numbers to strings + elif isinstance(value, num_types): + if isinstance(value, float) and key[2] == "vary_rounds": + value = ("%.2f" % value).rstrip("0") if value else "0" + else: + value = str(value) + + assert isinstance(value, native_string_types), \ + "expected string for key: %r %r" % (key, value) + + # escape any percent signs. + return value.replace("%", "%%") + + def to_dict(self, resolve=False): + """Return current configuration as a dictionary. + + :type resolve: bool + :arg resolve: + if ``True``, the ``schemes`` key will contain a list of + a :class:`~passlib.ifc.PasswordHash` objects instead of just + their names. + + This method dumps the current configuration of the CryptContext + instance. The key/value pairs should be in the format accepted + by the :class:`!CryptContext` class constructor, in fact + ``CryptContext(**myctx.to_dict())`` will create an exact copy of ``myctx``. + As an example:: + + >>> # you can dump the configuration of any crypt context... + >>> from passlib.apps import ldap_nocrypt_context + >>> ldap_nocrypt_context.to_dict() + {'schemes': ['ldap_salted_sha1', + 'ldap_salted_md5', + 'ldap_sha1', + 'ldap_md5', + 'ldap_plaintext']} + + .. versionadded:: 1.6 + This was previously available as ``CryptContext().policy.to_dict()`` + + .. seealso:: the :ref:`context-serialization-example` example in the tutorial. + """ + # XXX: should resolve default to conditional behavior + # based on presence of unregistered handlers? + render_key = self._render_config_key + return dict((render_key(key), value) + for key, value in self._config.iter_config(resolve)) + + def _write_to_parser(self, parser, section): + """helper to write to ConfigParser instance""" + render_key = self._render_config_key + render_value = self._render_ini_value + parser.add_section(section) + for k,v in self._config.iter_config(): + v = render_value(k, v) + k = render_key(k) + parser.set(section, k, v) + + def to_string(self, section="passlib"): + """serialize to INI format and return as unicode string. + + :param section: + name of INI section to output, defaults to ``"passlib"``. + + :returns: + CryptContext configuration, serialized to a INI unicode string. + + This function acts exactly like :meth:`to_dict`, except that it + serializes all the contents into a single human-readable string, + which can be hand edited, and/or stored in a file. The + output of this method is accepted by :meth:`from_string`, + :meth:`from_path`, and :meth:`load`. As an example:: + + >>> # you can dump the configuration of any crypt context... + >>> from passlib.apps import ldap_nocrypt_context + >>> print ldap_nocrypt_context.to_string() + [passlib] + schemes = ldap_salted_sha1, ldap_salted_md5, ldap_sha1, ldap_md5, ldap_plaintext + + .. versionadded:: 1.6 + This was previously available as ``CryptContext().policy.to_string()`` + + .. seealso:: the :ref:`context-serialization-example` example in the tutorial. + """ + parser = SafeConfigParser() + self._write_to_parser(parser, section) + buf = NativeStringIO() + parser.write(buf) + unregistered = self._get_unregistered_handlers() + if unregistered: + buf.write(( + "# NOTE: the %s handler(s) are not registered with Passlib,\n" + "# this string may not correctly reproduce the current configuration.\n\n" + ) % ", ".join(repr(handler.name) for handler in unregistered)) + out = buf.getvalue() + if not PY3: + out = out.decode("utf-8") + return out + + # XXX: is this useful enough to enable? + ##def write_to_path(self, path, section="passlib", update=False): + ## "write to INI file" + ## parser = ConfigParser() + ## if update and os.path.exists(path): + ## if not parser.read([path]): + ## raise EnvironmentError("failed to read existing file") + ## parser.remove_section(section) + ## self._write_to_parser(parser, section) + ## fh = file(path, "w") + ## parser.write(fh) + ## fh.close() + + #=================================================================== + # verify() hardening + # NOTE: this entire feature has been disabled. + # all contents of this section are NOOPs as of 1.7.1, + # and will be removed in 1.8. + #=================================================================== + + mvt_estimate_max_samples = 20 + mvt_estimate_min_samples = 10 + mvt_estimate_max_time = 2 + mvt_estimate_resolution = 0.01 + harden_verify = None + min_verify_time = 0 + + def reset_min_verify_time(self): + self._reset_dummy_verify() + + #=================================================================== + # password hash api + #=================================================================== + + # NOTE: all the following methods do is look up the appropriate + # custom handler for a given (scheme,category) combination, + # and hand off the real work to the handler itself, + # which is optimized for the specific (scheme,category) configuration. + # + # The custom handlers are cached inside the _CryptConfig + # instance stored in self._config, and are retrieved + # via get_record() and identify_record(). + # + # _get_record() and _identify_record() are references + # to _config methods of the same name, + # stored in CryptContext for speed. + + def _get_or_identify_record(self, hash, scheme=None, category=None): + """return record based on scheme, or failing that, by identifying hash""" + if scheme: + if not isinstance(hash, unicode_or_bytes_types): + raise ExpectedStringError(hash, "hash") + return self._get_record(scheme, category) + else: + # hash typecheck handled by identify_record() + return self._identify_record(hash, category) + + def _strip_unused_context_kwds(self, kwds, record): + """ + helper which removes any context keywords from **kwds** + that are known to be used by another scheme in this context, + but are NOT supported by handler specified by **record**. + + .. note:: + as optimization, load() will set this method to None on a per-instance basis + if there are no context kwds. + """ + if not kwds: + return + unused_kwds = self._config.context_kwds.difference(record.context_kwds) + for key in unused_kwds: + kwds.pop(key, None) + + def needs_update(self, hash, scheme=None, category=None, secret=None): + """Check if hash needs to be replaced for some reason, + in which case the secret should be re-hashed. + + This function is the core of CryptContext's support for hash migration: + This function takes in a hash string, and checks the scheme, + number of rounds, and other properties against the current policy. + It returns ``True`` if the hash is using a deprecated scheme, + or is otherwise outside of the bounds specified by the policy + (e.g. the number of rounds is lower than :ref:`min_rounds ` + configuration for that algorithm). + If so, the password should be re-hashed using :meth:`hash` + Otherwise, it will return ``False``. + + :type hash: unicode or bytes + :arg hash: + The hash string to examine. + + :type scheme: str or None + :param scheme: + + Optional scheme to use. Scheme must be one of the ones + configured for this context (see the + :ref:`schemes ` option). + If no scheme is specified, it will be identified + based on the value of *hash*. + + .. deprecated:: 1.7 + + Support for this keyword is deprecated, and will be removed in Passlib 2.0. + + :type category: str or None + :param category: + Optional :ref:`user category `. + If specified, this will cause any category-specific defaults to + be used when determining if the hash needs to be updated + (e.g. is below the minimum rounds). + + :type secret: unicode, bytes, or None + :param secret: + Optional secret associated with the provided ``hash``. + This is not required, or even currently used for anything... + it's for forward-compatibility with any future + update checks that might need this information. + If provided, Passlib assumes the secret has already been + verified successfully against the hash. + + .. versionadded:: 1.6 + + :returns: ``True`` if hash should be replaced, otherwise ``False``. + + :raises ValueError: + If the hash did not match any of the configured :meth:`schemes`. + + .. versionadded:: 1.6 + This method was previously named :meth:`hash_needs_update`. + + .. seealso:: the :ref:`context-migration-example` example in the tutorial. + """ + if scheme is not None: + # TODO: offer replacement alternative. + # ``context.handler(scheme).needs_update()`` would work, + # but may deprecate .handler() in passlib 1.8. + warn("CryptContext.needs_update(): 'scheme' keyword is deprecated as of " + "Passlib 1.7, and will be removed in Passlib 2.0", + DeprecationWarning) + record = self._get_or_identify_record(hash, scheme, category) + return record.deprecated or record.needs_update(hash, secret=secret) + + @deprecated_method(deprecated="1.6", removed="2.0", replacement="CryptContext.needs_update()") + def hash_needs_update(self, hash, scheme=None, category=None): + """Legacy alias for :meth:`needs_update`. + + .. deprecated:: 1.6 + This method was renamed to :meth:`!needs_update` in version 1.6. + This alias will be removed in version 2.0, and should only + be used for compatibility with Passlib 1.3 - 1.5. + """ + return self.needs_update(hash, scheme, category) + + @deprecated_method(deprecated="1.7", removed="2.0") + def genconfig(self, scheme=None, category=None, **settings): + """Generate a config string for specified scheme. + + .. deprecated:: 1.7 + + This method will be removed in version 2.0, and should only + be used for compatibility with Passlib 1.3 - 1.6. + """ + record = self._get_record(scheme, category) + strip_unused = self._strip_unused_context_kwds + if strip_unused: + strip_unused(settings, record) + return record.genconfig(**settings) + + @deprecated_method(deprecated="1.7", removed="2.0") + def genhash(self, secret, config, scheme=None, category=None, **kwds): + """Generate hash for the specified secret using another hash. + + .. deprecated:: 1.7 + + This method will be removed in version 2.0, and should only + be used for compatibility with Passlib 1.3 - 1.6. + """ + record = self._get_or_identify_record(config, scheme, category) + strip_unused = self._strip_unused_context_kwds + if strip_unused: + strip_unused(kwds, record) + return record.genhash(secret, config, **kwds) + + def identify(self, hash, category=None, resolve=False, required=False, + unconfigured=False): + """Attempt to identify which algorithm the hash belongs to. + + Note that this will only consider the algorithms + currently configured for this context + (see the :ref:`schemes ` option). + All registered algorithms will be checked, from first to last, + and whichever one positively identifies the hash first will be returned. + + :type hash: unicode or bytes + :arg hash: + The hash string to test. + + :type category: str or None + :param category: + Optional :ref:`user category `. + Ignored by this function, this parameter + is provided for symmetry with the other methods. + + :type resolve: bool + :param resolve: + If ``True``, returns the hash handler itself, + instead of the name of the hash. + + :type required: bool + :param required: + If ``True``, this will raise a ValueError if the hash + cannot be identified, instead of returning ``None``. + + :returns: + The handler which first identifies the hash, + or ``None`` if none of the algorithms identify the hash. + """ + record = self._identify_record(hash, category, required) + if record is None: + return None + elif resolve: + if unconfigured: + return record._Context__orig_handler + else: + return record + else: + return record.name + + def hash(self, secret, scheme=None, category=None, **kwds): + """run secret through selected algorithm, returning resulting hash. + + :type secret: unicode or bytes + :arg secret: + the password to hash. + + :type scheme: str or None + :param scheme: + + Optional scheme to use. Scheme must be one of the ones + configured for this context (see the + :ref:`schemes ` option). + If no scheme is specified, the configured default + will be used. + + .. deprecated:: 1.7 + + Support for this keyword is deprecated, and will be removed in Passlib 2.0. + + :type category: str or None + :param category: + Optional :ref:`user category `. + If specified, this will cause any category-specific defaults to + be used when hashing the password (e.g. different default scheme, + different default rounds values, etc). + + :param \\*\\*kwds: + All other keyword options are passed to the selected algorithm's + :meth:`PasswordHash.hash() ` method. + + :returns: + The secret as encoded by the specified algorithm and options. + The return value will always be a :class:`!str`. + + :raises TypeError, ValueError: + * If any of the arguments have an invalid type or value. + This includes any keywords passed to the underlying hash's + :meth:`PasswordHash.hash() ` method. + + .. seealso:: the :ref:`context-basic-example` example in the tutorial + """ + # XXX: could insert normalization to preferred unicode encoding here + if scheme is not None: + # TODO: offer replacement alternative. + # ``context.handler(scheme).hash()`` would work, + # but may deprecate .handler() in passlib 1.8. + warn("CryptContext.hash(): 'scheme' keyword is deprecated as of " + "Passlib 1.7, and will be removed in Passlib 2.0", + DeprecationWarning) + record = self._get_record(scheme, category) + strip_unused = self._strip_unused_context_kwds + if strip_unused: + strip_unused(kwds, record) + return record.hash(secret, **kwds) + + @deprecated_method(deprecated="1.7", removed="2.0", replacement="CryptContext.hash()") + def encrypt(self, *args, **kwds): + """ + Legacy alias for :meth:`hash`. + + .. deprecated:: 1.7 + This method was renamed to :meth:`!hash` in version 1.7. + This alias will be removed in version 2.0, and should only + be used for compatibility with Passlib 1.3 - 1.6. + """ + return self.hash(*args, **kwds) + + def verify(self, secret, hash, scheme=None, category=None, **kwds): + """verify secret against an existing hash. + + If no scheme is specified, this will attempt to identify + the scheme based on the contents of the provided hash + (limited to the schemes configured for this context). + It will then check whether the password verifies against the hash. + + :type secret: unicode or bytes + :arg secret: + the secret to verify + + :type hash: unicode or bytes + :arg hash: + hash string to compare to + + if ``None`` is passed in, this will be treated as "never verifying" + + :type scheme: str + :param scheme: + Optionally force context to use specific scheme. + This is usually not needed, as most hashes can be unambiguously + identified. Scheme must be one of the ones configured + for this context + (see the :ref:`schemes ` option). + + .. deprecated:: 1.7 + + Support for this keyword is deprecated, and will be removed in Passlib 2.0. + + :type category: str or None + :param category: + Optional :ref:`user category ` string. + This is mainly used when generating new hashes, it has little + effect when verifying; this keyword is mainly provided for symmetry. + + :param \\*\\*kwds: + All additional keywords are passed to the appropriate handler, + and should match its :attr:`~passlib.ifc.PasswordHash.context_kwds`. + + :returns: + ``True`` if the password matched the hash, else ``False``. + + :raises ValueError: + * if the hash did not match any of the configured :meth:`schemes`. + + * if any of the arguments have an invalid value (this includes + any keywords passed to the underlying hash's + :meth:`PasswordHash.verify() ` method). + + :raises TypeError: + * if any of the arguments have an invalid type (this includes + any keywords passed to the underlying hash's + :meth:`PasswordHash.verify() ` method). + + .. seealso:: the :ref:`context-basic-example` example in the tutorial + """ + # XXX: could insert normalization to preferred unicode encoding here + # XXX: what about supporting a setter() callback ala django 1.4 ? + if scheme is not None: + # TODO: offer replacement alternative. + # ``context.handler(scheme).verify()`` would work, + # but may deprecate .handler() in passlib 1.8. + warn("CryptContext.verify(): 'scheme' keyword is deprecated as of " + "Passlib 1.7, and will be removed in Passlib 2.0", + DeprecationWarning) + if hash is None: + # convenience feature -- let apps pass in hash=None when user + # isn't found / has no hash; useful because it invokes dummy_verify() + self.dummy_verify() + return False + record = self._get_or_identify_record(hash, scheme, category) + strip_unused = self._strip_unused_context_kwds + if strip_unused: + strip_unused(kwds, record) + return record.verify(secret, hash, **kwds) + + def verify_and_update(self, secret, hash, scheme=None, category=None, **kwds): + """verify password and re-hash the password if needed, all in a single call. + + This is a convenience method which takes care of all the following: + first it verifies the password (:meth:`~CryptContext.verify`), if this is successfull + it checks if the hash needs updating (:meth:`~CryptContext.needs_update`), and if so, + re-hashes the password (:meth:`~CryptContext.hash`), returning the replacement hash. + This series of steps is a very common task for applications + which wish to update deprecated hashes, and this call takes + care of all 3 steps efficiently. + + :type secret: unicode or bytes + :arg secret: + the secret to verify + + :type secret: unicode or bytes + :arg hash: + hash string to compare to. + + if ``None`` is passed in, this will be treated as "never verifying" + + :type scheme: str + :param scheme: + Optionally force context to use specific scheme. + This is usually not needed, as most hashes can be unambiguously + identified. Scheme must be one of the ones configured + for this context + (see the :ref:`schemes ` option). + + .. deprecated:: 1.7 + + Support for this keyword is deprecated, and will be removed in Passlib 2.0. + + :type category: str or None + :param category: + Optional :ref:`user category `. + If specified, this will cause any category-specific defaults to + be used if the password has to be re-hashed. + + :param \\*\\*kwds: + all additional keywords are passed to the appropriate handler, + and should match that hash's + :attr:`PasswordHash.context_kwds `. + + :returns: + This function returns a tuple containing two elements: + ``(verified, replacement_hash)``. The first is a boolean + flag indicating whether the password verified, + and the second an optional replacement hash. + The tuple will always match one of the following 3 cases: + + * ``(False, None)`` indicates the secret failed to verify. + * ``(True, None)`` indicates the secret verified correctly, + and the hash does not need updating. + * ``(True, str)`` indicates the secret verified correctly, + but the current hash needs to be updated. The :class:`!str` + will be the freshly generated hash, to replace the old one. + + :raises TypeError, ValueError: + For the same reasons as :meth:`verify`. + + .. seealso:: the :ref:`context-migration-example` example in the tutorial. + """ + # XXX: could insert normalization to preferred unicode encoding here. + if scheme is not None: + warn("CryptContext.verify(): 'scheme' keyword is deprecated as of " + "Passlib 1.7, and will be removed in Passlib 2.0", + DeprecationWarning) + if hash is None: + # convenience feature -- let apps pass in hash=None when user + # isn't found / has no hash; useful because it invokes dummy_verify() + self.dummy_verify() + return False, None + record = self._get_or_identify_record(hash, scheme, category) + strip_unused = self._strip_unused_context_kwds + if strip_unused and kwds: + clean_kwds = kwds.copy() + strip_unused(clean_kwds, record) + else: + clean_kwds = kwds + # XXX: if record is default scheme, could extend PasswordHash + # api to combine verify & needs_update to single call, + # potentially saving some round-trip parsing. + # but might make these codepaths more complex... + if not record.verify(secret, hash, **clean_kwds): + return False, None + elif record.deprecated or record.needs_update(hash, secret=secret): + # NOTE: we re-hash with default scheme, not current one. + return True, self.hash(secret, category=category, **kwds) + else: + return True, None + + #=================================================================== + # missing-user helper + #=================================================================== + + #: secret used for dummy_verify() + _dummy_secret = "too many secrets" + + @memoized_property + def _dummy_hash(self): + """ + precalculated hash for dummy_verify() to use + """ + return self.hash(self._dummy_secret) + + def _reset_dummy_verify(self): + """ + flush memoized values used by dummy_verify() + """ + type(self)._dummy_hash.clear_cache(self) + + def dummy_verify(self, elapsed=0): + """ + Helper that applications can call when user wasn't found, + in order to simulate time it would take to hash a password. + + Runs verify() against a dummy hash, to simulate verification + of a real account password. + + :param elapsed: + + .. deprecated:: 1.7.1 + + this option is ignored, and will be removed in passlib 1.8. + + .. versionadded:: 1.7 + """ + self.verify(self._dummy_secret, self._dummy_hash) + return False + + #=================================================================== + # disabled hash support + #=================================================================== + + def is_enabled(self, hash): + """ + test if hash represents a usuable password -- + i.e. does not represent an unusuable password such as ``"!"``, + which is recognized by the :class:`~passlib.hash.unix_disabled` hash. + + :raises ValueError: + if the hash is not recognized + (typically solved by adding ``unix_disabled`` to the list of schemes). + """ + return not self._identify_record(hash, None).is_disabled + + def disable(self, hash=None): + """ + return a string to disable logins for user, + usually by returning a non-verifying string such as ``"!"``. + + :param hash: + Callers can optionally provide the account's existing hash. + Some disabled handlers (such as :class:`!unix_disabled`) + will encode this into the returned value, + so that it can be recovered via :meth:`enable`. + + :raises RuntimeError: + if this function is called w/o a disabled hasher + (such as :class:`~passlib.hash.unix_disabled`) included + in the list of schemes. + + :returns: + hash string which will be recognized as valid by the context, + but is guaranteed to not validate against *any* password. + """ + record = self._config.disabled_record + assert record.is_disabled + return record.disable(hash) + + def enable(self, hash): + """ + inverse of :meth:`disable` -- + attempts to recover original hash which was converted + by a :meth:`!disable` call into a disabled hash -- + thus restoring the user's original password. + + :raises ValueError: + if original hash not present, or if the disabled handler doesn't + support encoding the original hash (e.g. ``django_disabled``) + + :returns: + the original hash. + """ + record = self._identify_record(hash, None) + if record.is_disabled: + # XXX: should we throw error if result can't be identified by context? + return record.enable(hash) + else: + # hash wasn't a disabled hash, so return unchanged + return hash + + #=================================================================== + # eoc + #=================================================================== + +class LazyCryptContext(CryptContext): + """CryptContext subclass which doesn't load handlers until needed. + + This is a subclass of CryptContext which takes in a set of arguments + exactly like CryptContext, but won't import any handlers + (or even parse its arguments) until + the first time one of its methods is accessed. + + :arg schemes: + The first positional argument can be a list of schemes, or omitted, + just like CryptContext. + + :param onload: + + If a callable is passed in via this keyword, + it will be invoked at lazy-load time + with the following signature: + ``onload(**kwds) -> kwds``; + where ``kwds`` is all the additional kwds passed to LazyCryptContext. + It should perform any additional deferred initialization, + and return the final dict of options to be passed to CryptContext. + + .. versionadded:: 1.6 + + :param create_policy: + + .. deprecated:: 1.6 + This option will be removed in Passlib 1.8, + applications should use ``onload`` instead. + + :param kwds: + + All additional keywords are passed to CryptContext; + or to the *onload* function (if provided). + + This is mainly used internally by modules such as :mod:`passlib.apps`, + which define a large number of contexts, but only a few of them will be needed + at any one time. Use of this class saves the memory needed to import + the specified handlers until the context instance is actually accessed. + As well, it allows constructing a context at *module-init* time, + but using :func:`!onload()` to provide dynamic configuration + at *application-run* time. + + .. note:: + This class is only useful if you're referencing handler objects by name, + and don't want them imported until runtime. If you want to have the config + validated before your application runs, or are passing in already-imported + handler instances, you should use :class:`CryptContext` instead. + + .. versionadded:: 1.4 + """ + _lazy_kwds = None + + # NOTE: the way this class works changed in 1.6. + # previously it just called _lazy_init() when ``.policy`` was + # first accessed. now that is done whenever any of the public + # attributes are accessed, and the class itself is changed + # to a regular CryptContext, to remove the overhead once it's unneeded. + + def __init__(self, schemes=None, **kwds): + if schemes is not None: + kwds['schemes'] = schemes + self._lazy_kwds = kwds + + def _lazy_init(self): + kwds = self._lazy_kwds + if 'create_policy' in kwds: + warn("The CryptPolicy class, and LazyCryptContext's " + "``create_policy`` keyword have been deprecated as of " + "Passlib 1.6, and will be removed in Passlib 1.8; " + "please use the ``onload`` keyword instead.", + DeprecationWarning) + create_policy = kwds.pop("create_policy") + result = create_policy(**kwds) + policy = CryptPolicy.from_source(result, _warn=False) + kwds = policy._context.to_dict() + elif 'onload' in kwds: + onload = kwds.pop("onload") + kwds = onload(**kwds) + del self._lazy_kwds + super(LazyCryptContext, self).__init__(**kwds) + self.__class__ = CryptContext + + def __getattribute__(self, attr): + if (not attr.startswith("_") or attr.startswith("__")) and \ + self._lazy_kwds is not None: + self._lazy_init() + return object.__getattribute__(self, attr) + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/crypto/__init__.py b/ansible/lib/python3.11/site-packages/passlib/crypto/__init__.py new file mode 100644 index 000000000..89f54847e --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/crypto/__init__.py @@ -0,0 +1 @@ +"""passlib.crypto -- package containing cryptographic primitives used by passlib""" diff --git a/ansible/lib/python3.11/site-packages/passlib/crypto/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/crypto/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cb9b01e03e1645963703c1265eb84daa57e51ca4 GIT binary patch literal 281 zcmXw!y-EZz5P%aE72yisAng^q!(l7Ffr6ZsO~@u8JGh&KOjcN5#LiaieGp5r^#z3M zt}=VKHw-hGpUgM&zF1taB5&@~Jrw^t1qb2tV>7k9GQ%F3;cK=$dby2?sKch#X^c6D zsuEG@C*`cr!DodYy%YPQODeXgMNHUZ#%D`nAZtW3mOr~GcT4_rCZ!2l$`7`gsM~vJ zLvMk4HKYJB1p~?xHl2mSg*fIm_^W!g0>W&onNBc|ggqOiEURQG>o{)Dulrzz&fcWc WLYR)D%VPeHIp^PO{dLfEQnLSo995qH literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/crypto/__pycache__/_md4.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/crypto/__pycache__/_md4.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..91b0c5b3c64e92b9a82beb576979b8b1ec17219b GIT binary patch literal 8529 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

LnFfA|meUwJE7gNV!xF6o~MyXh0LF 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 X?C!DFt%xv~v<4#)gB literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/crypto/__pycache__/digest.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/crypto/__pycache__/digest.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c01a2308e0005209ead584e5d02571cb04504834 GIT binary patch literal 33156 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 literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/crypto/_blowfish/__init__.py b/ansible/lib/python3.11/site-packages/passlib/crypto/_blowfish/__init__.py new file mode 100644 index 000000000..1aa1c85f7 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/crypto/_blowfish/__init__.py @@ -0,0 +1,169 @@ +"""passlib.crypto._blowfish - pure-python eks-blowfish implementation for bcrypt + +This is a pure-python implementation of the EKS-Blowfish algorithm described by +Provos and Mazieres in `A Future-Adaptable Password Scheme +`_. + +This package contains two submodules: + +* ``_blowfish/base.py`` contains a class implementing the eks-blowfish algorithm + using easy-to-examine code. + +* ``_blowfish/unrolled.py`` contains a subclass which replaces some methods + of the original class with sped-up versions, mainly using unrolled loops + and local variables. this is the class which is actually used by + Passlib to perform BCrypt in pure python. + + This module is auto-generated by a script, ``_blowfish/_gen_files.py``. + +Status +------ +This implementation is usable, but is an order of magnitude too slow to be +usable with real security. For "ok" security, BCrypt hashes should have at +least 2**11 rounds (as of 2011). Assuming a desired response time <= 100ms, +this means a BCrypt implementation should get at least 20 rounds/ms in order +to be both usable *and* secure. On a 2 ghz cpu, this implementation gets +roughly 0.09 rounds/ms under CPython (220x too slow), and 1.9 rounds/ms +under PyPy (10x too slow). + +History +------- +While subsequently modified considerly for Passlib, this code was originally +based on `jBcrypt 0.2 `_, which was +released under the BSD license:: + + Copyright (c) 2006 Damien Miller + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +""" +#============================================================================= +# imports +#============================================================================= +# core +from itertools import chain +import struct +# pkg +from passlib.utils import getrandbytes, rng +from passlib.utils.binary import bcrypt64 +from passlib.utils.compat import BytesIO, unicode, u, native_string_types +from passlib.crypto._blowfish.unrolled import BlowfishEngine +# local +__all__ = [ + 'BlowfishEngine', + 'raw_bcrypt', +] + +#============================================================================= +# bcrypt constants +#============================================================================= + +# bcrypt constant data "OrpheanBeholderScryDoubt" as 6 integers +BCRYPT_CDATA = [ + 0x4f727068, 0x65616e42, 0x65686f6c, + 0x64657253, 0x63727944, 0x6f756274 +] + +# struct used to encode ciphertext as digest (last output byte discarded) +digest_struct = struct.Struct(">6I") + +#============================================================================= +# base bcrypt helper +# +# interface designed only for use by passlib.handlers.bcrypt:BCrypt +# probably not suitable for other purposes +#============================================================================= +BNULL = b'\x00' + +def raw_bcrypt(password, ident, salt, log_rounds): + """perform central password hashing step in bcrypt scheme. + + :param password: the password to hash + :param ident: identifier w/ minor version (e.g. 2, 2a) + :param salt: the binary salt to use (encoded in bcrypt-base64) + :param log_rounds: the log2 of the number of rounds (as int) + :returns: bcrypt-base64 encoded checksum + """ + #=================================================================== + # parse inputs + #=================================================================== + + # parse ident + assert isinstance(ident, native_string_types) + add_null_padding = True + if ident == u('2a') or ident == u('2y') or ident == u('2b'): + pass + elif ident == u('2'): + add_null_padding = False + elif ident == u('2x'): + raise ValueError("crypt_blowfish's buggy '2x' hashes are not " + "currently supported") + else: + raise ValueError("unknown ident: %r" % (ident,)) + + # decode & validate salt + assert isinstance(salt, bytes) + salt = bcrypt64.decode_bytes(salt) + if len(salt) < 16: + raise ValueError("Missing salt bytes") + elif len(salt) > 16: + salt = salt[:16] + + # prepare password + assert isinstance(password, bytes) + if add_null_padding: + password += BNULL + + # validate rounds + if log_rounds < 4 or log_rounds > 31: + raise ValueError("Bad number of rounds") + + #=================================================================== + # + # run EKS-Blowfish algorithm + # + # This uses the "enhanced key schedule" step described by + # Provos and Mazieres in "A Future-Adaptable Password Scheme" + # http://www.openbsd.org/papers/bcrypt-paper.ps + # + #=================================================================== + + engine = BlowfishEngine() + + # convert password & salt into list of 18 32-bit integers (72 bytes total). + pass_words = engine.key_to_words(password) + salt_words = engine.key_to_words(salt) + + # truncate salt_words to original 16 byte salt, or loop won't wrap + # correctly when passed to .eks_salted_expand() + salt_words16 = salt_words[:4] + + # do EKS key schedule setup + engine.eks_salted_expand(pass_words, salt_words16) + + # apply password & salt keys to key schedule a bunch more times. + rounds = 1<fTk<5OU9=4fCEiH1DU#(Q zjtsf*iVkp#VQ_#HGd6U{hPCU^pM3PA13F;)7_d(%&`^OufdRul^5-Gz&wjRZFR7Q8 ztu#$t-h1A!doF)7Fu*Zz{i673@%T8y{EfV{u0Ch*WFH1UWdtV62rj`bxytUW+eOBG zQeU{SmHNwEmMaIcfpRb#EDvM{=)6xFEDvReVC)zA zi|*`jkrB9Gxw0c}2KTKWX$c_&K5{Y4C-84y*-;^w9TNuLVY1H%eQ9QLkpGjPVN_n% zB{3f=Xcg5^Lb<%8Y~B|25}HP;so`n0Vw4ma;SGJdy(E@Z374^K@P-I$w-pWLDL%(# zN}`V7=6kRYAt<+zQNk$pT6%i1#g3PXiY6MRG7_*}(8N3zP`<*gY04c1$jJg)<+ntv zVIVJ~TM@Ku8brQ`z^ewImoQobO*R!xKECfmUx=Jg;agvRxad-ukwWrzKBsl0S!f2N5-atbTeO81XIHLJjY!^w{Ep970&ZI z4ylz}w>lU+Do7v>YAY_vMXGa;9osr{2$?zo!Mt9XHk4_6moJMl5kbHpRxdwO))Yy? z!hi7u8Ev+kC9zOK8dfE~00z^QGDc+#judoY<(MDLT@+ye1Mi=poS4jsfE1Zh1&p6bzG>C_UT1h`J2| z5E%*rzd$UN(PESYBk?l{L1c#@m;oUgUbY`-l$l_wB9^hn8Ed!rhpHFcuV6v~?*3<*L6;e|C~z%pNyMZ**@$PK{(QA`xb zW6s7w)S4Ow!|1qRLSR=yXc_Y4jIwd2JvG(Rw8ZNr;-`{gN&+hJcQE1&PJ(DK(A=d< zv$IH3Oj*#;MP4V|=VoSSCqpQr>n4Or5wwP+5j8LY)b z%9w{}=389b!D*+l!@YH?O)>1sqbcjSVCdm>JHo?Ibf%D< ztU#F4FtIuiwDlqRvzT5&l32jt(s?L62(GB2RzQN1fi4y%ArxjVqa~;oSVpTNlm!i4 z5!TDEcJW=X@z$_b7Il)=Bvefu0=odDAjOIL@Jo_e0Bb?7#pjhfn35tA z*viO?0eVs)C}$`JEpb~*s)~7ylm#duM0OGK+14#{Er!y`<;)wAR1C$_Xf2g|J-!rM zLT4grct3+8i6umUA~!PEk}0$lPe)fG@zpentgIlwN<|Wxcq~n2dLy2>hElPskrd!0 z0SyRth$o^eH1#xUF4-1iXeAz5T#2FOWQx*?ByOVgS}YomtW1IEsaP~K1vFYC&?lNq zq+{Q?0rUU{Ek#x%S7T`e3k219wRO7|$)uCOIRzS~Z>(g9Cd;YhDq2aV0b?26NXLLl zBoiU%U`G%-J%!%576TDeMDj?&7Al&FCldq%m}gQEB2yxEbtQf^mWaj(a*|?Xk|_YZ z0hXo+Qz(*(rwP~OjZDi}01SkIP6AXTRH+MyO+XqdM=S-#SdCE1%e^jv5O_>loJp7E zDU@KlN=&+~Ak-I#KOtW+ux|BfauJsO_MU(F#YxukEs~XZ((;+INK)H!nbxQbrSJ~U zksOeVIisRt9q?lC;M!&Z z%F}@o+`S#&2F1|NAA;(8D%9v9DE4VknoGWZ2+s3VxIV$V&B`wxg7rNWcF6E`>EQ2$ zdE3>q+Fy13-2K5JJprcMa&80K+Sc_n%NfLf&b3Z_K(Fc{s@ra3sI>$a!rc|cD-?I~waWqPS>d;cfoAzN#JA`g^?!C!yhL-xg%jc0XVRJFuM*VEL;39|6oKB;)AiVvG~5F~b-qJL5x| zaKgxTpc)X4I6i*`xODlogIDFMfsPN!n=rfu%bhu4v{#1jLyi)-D*)Ajr*8pajQX&P z^TSt+XFJ^1M_S`5_Y(#z_)L!lgFSKHJ{MMlz1qP^@Ed#@g00nkL$d*;Q$plqNY7KG zJ0)GFZeW${zIM@rMw=Y7oDFzh-%C6u+5Y%6Ssq*D1$0=o$E3z+WSe+Q>fqy7 z$%DTB8?MPwixYvQZt!veYb5Knet1~i>%3&*n5HS3<)NH3f^G!`Ol+TXHeCtJmS-_> zV`atOF@qbpk~5SX2`b$hfunGa$clv=zN^AsZHjMMqo|Li|L+kaRk{Bog;Y8IM^A_Ubeh;$XKkv8@%Nbq4VH3b*RJ)>D=4b zzrGhP!I?a)mw8iD!q6ZKVK|zJaM}#Rfg^06cwY$3&W3f-fR8Zt7YQA{UC^QP>L0K5kAFFM?0|6{5C4O44bA+M zJm9r|<|{8VG`_q3(DTq!8~;{)@TJD!OEv#XFb$FlAH?c`GmXHR8tdF&21XkJ^kA$W zINu1I-&y+7$L+rO!^%6Ay}7sF+Ib6@)W)algQ3P?sOArS*&p1^ygUBm@x9c0r*=;@ zher3Jd#SzXk1M;Z&7t(6$0k){&)av?cN7Hk9B=xE-;dS(dv?I=j$d7i#PRQDyl3U_CI=2u$oOHU0fNktP?|<=;EE zd#*Y0+!qrsJeqjn;nHU#pI-ee^M_~u5c%VCpWpnmyFT$+W8$^C|M)Ju`<-`vdt-b2 zzUyA$-Z)Tz3Gc4!z{3QBHXUFRIM(1!?h7^Uxd+h)sRz*pcX21`cx1F5IM)cAtFh;v zK)k_jV)=48K`G>NRzC!<1|LWyT_d}$<#qCNz=`9KX^5~!s+2|=S!=9kA!OHgySN-$ zDkx=@H#E{ZYNX$zm5J6U>)93&J4-s>G()ZLS9U!DG2tMY%W3BcYj2v?4(%c#Ms``6 zLce9Sm&y1gc>K$M#s2iPpe?~Gsh#@Y;c?LCa=8xNGcNXkA&+$z<2qSmPITW*W?+Z@ z{grvX_O!dnOxAkuCKIoH-Q8ra)Ov3cOwTmg;R8=!z`f%;80#B$A26-QPVgYejGWr@ j-XGY<_2ILP;j=pf2g6MN@Q&wzU3Iz6A23gUQZxJqyOnmS literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/crypto/_blowfish/__pycache__/_gen_files.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/crypto/_blowfish/__pycache__/_gen_files.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2ac5abec754340a533ee891842b85b1c540928e6 GIT binary patch literal 8396 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 literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/crypto/_blowfish/__pycache__/base.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/crypto/_blowfish/__pycache__/base.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fadca61a566e2b550887e5b56520f473a57d9878 GIT binary patch literal 14769 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 literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/crypto/_blowfish/__pycache__/unrolled.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/crypto/_blowfish/__pycache__/unrolled.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ae3176a3649d98162b870f564ac9cadb5feb2e26 GIT binary patch literal 41718 zcmeGlOKcn0@$K>>k`gJBk|;{F9NJDT%e6&Qk|n1O5=d_A)(wJI@};T^g5s_u#uUly zQnsb;B0i)51^BQk41@xLfVs3ZdeI?=_~zVlv;ZFno(%Lvpap2Sm*(QmTS`l=BC`cd zJ-XUjq-kUd1Z{N(k-8b_$x7$VF^VQNfOD8r7`3FVzKDON-&o9A`FNi?qiNFbb zid*65EgTUnOWeGbCs&DJ{TwjS@>dHXe}|*~ndb%D92v2%|0^4pDwD~ElPlSjxFV)=@mw;K4rdm_3wq&muJ~#$vm~ZP zDV~FJ3v1yRV6nwyN|Z;lYc5w}Rg$1wYAu{wPRilTEm4w^f(V!x7SoAjc3G6pjD+K9 zAuQg>!WSSbVs1Gj$gbPT+;TXR%_Uco>rgoqm4Ul_PNz2N%iNsFVdDwTeOt9WL1rTj&DDGBYR{gSBnOdWE&$n!z?@dXjdJO^FO3*6^q-Xib- zt%3zQ&{{8Q^ea&N;h^~Q(+*lxA)f$J|J;CUsztS`Hr1|ns1DVsy3|g!OLeOrwOjS7 zKGm=GsJ&2TuL0zS70UFf0kvNZDx6|j9{`+Fc%@I_1a93)2%OmcC;>Rf({qAl9cpM? zSqBQGAFf&tyxIju*vy{YWf(%?cvw!Rmr`PQa^n0#G8YDl#7%lR zBY*)E2Xp6kCOkf-UrVNQay1A_zUrmm1qEP!kbhE3OOKC6 zqY*io6VGSkiJS2yQI70dt&zIb8ZoTa$eyVR#$Uy;XH%~Co9(^Py$gNNIJwhK$X!x% zkc;nq^UG%z&N-I%=Vvv)He2u)W{dvfY{_4mExX)#S-Vo`DI|*HB}-|pJTUZSV8fH= z^D`Q+%{+BD^RpWrd9G$BP8XD(F8DXE-VYU|Vzk76JyjBRPL4f!XMD#uzAb$leZ)PS ze02R`zl!QDQJKpV3WtfJ3DYUM zfx>B`Xu@;~FHpEl6it{;;RA|J6GaoIQ}}_R%S6$H=@dOc;Wkk;VLC-GPFnihcFnihcCKhH zdk+QI?b*-!xNgthzQJ{S_T#+ux;^aIL_Lps9`!uxdFAlz8(g<%KkwtZJ$w7+=&svC zOa|2RsOM47qn<}Sf6%TRUJnH%S?teXU z-mBO*xNgthzG=N~53v+c&!e74J&$@G^*ri%)bp>jesgr!?Stxoq30cnQ*kMsN|)kR zJW99XReXwH=}~$imT>?gBL|g%MH)FMAO!x*zYjw~vHYB9)<5>C#~{AWkTN9n-_!?+ zW|f?{8=rZ!A>5_M-ipRudhDNq;?sWi;~by%vmfV2)@o3WDMPychi}Md06L#yh8-~k zc?#SRo5y4kY&$jUy?!A8k(`6;XZJ!7@8zh_Gi2=Mn+V?&*Xp?~X!Z(;MwzVAR1a1C z7Y){2g?taBO4EB(Auchgb^wqdqXVhxfMx`#c2WSbf=CsbLaH7Lx&cU(xay<7XjzFy zfR<>9kE;Z@NVMF@af#AboY0ck?TjSI60J$1HA$x^c$b2oP;i<8y*e5KQ|h6hmjW6$ zSqf0lPXP@)Em3DuqUkcE5Cz957^2`f1@BN0rr-nxCn?aIOygBoEDM=C748$6lD;ff zbp59Va*JG?~F>FQg)v z2Ic5r7Xuidu~joYXO4T01hx;iQh1`u;R;=9#yxBg9-a`WI&Z0w;Bx)NxWj>UmPla~MvK z<~gjrIceX5zQ#!%Cv}|EaZ=ySKa4i-K7H{-Ds}5t$l5-8e2VK?hrfq>A6zk!SHsp zboQ}#V#gcSEZX#@y!UAx|{|8T9e~yyp6Ng u2nD@_`-po?e$)IPRHUu1X5l;eyB)QzWG4qQ91OMCGyTfHUvgb<&wl~A#?@v3 literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/crypto/_blowfish/_gen_files.py b/ansible/lib/python3.11/site-packages/passlib/crypto/_blowfish/_gen_files.py new file mode 100644 index 000000000..2f92cf851 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/crypto/_blowfish/_gen_files.py @@ -0,0 +1,204 @@ +"""passlib.crypto._blowfish._gen_files - meta script that generates unrolled.py""" +#============================================================================= +# imports +#============================================================================= +# core +import os +import textwrap +# pkg +from passlib.utils.compat import irange +# local + +#============================================================================= +# helpers +#============================================================================= +def varlist(name, count): + return ", ".join(name + str(x) for x in irange(count)) + + +def indent_block(block, padding): + """ident block of text""" + lines = block.split("\n") + return "\n".join( + padding + line if line else "" + for line in lines + ) + +BFSTR = """\ + ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) +""".strip() + +def render_encipher(write, indent=0): + for i in irange(0, 15, 2): + write(indent, """\ + # Feistel substitution on left word (round %(i)d) + r ^= %(left)s ^ p%(i1)d + + # Feistel substitution on right word (round %(i1)d) + l ^= %(right)s ^ p%(i2)d + """, i=i, i1=i+1, i2=i+2, + left=BFSTR, right=BFSTR.replace("l","r"), + ) + +def write_encipher_function(write, indent=0): + write(indent, """\ + def encipher(self, l, r): + \"""blowfish encipher a single 64-bit block encoded as two 32-bit ints\""" + + (p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, + p10, p11, p12, p13, p14, p15, p16, p17) = self.P + S0, S1, S2, S3 = self.S + + l ^= p0 + + """) + render_encipher(write, indent+1) + + write(indent+1, """\ + + return r ^ p17, l + + """) + +def write_expand_function(write, indent=0): + write(indent, """\ + def expand(self, key_words): + \"""unrolled version of blowfish key expansion\""" + ##assert len(key_words) >= 18, "size of key_words must be >= 18" + + P, S = self.P, self.S + S0, S1, S2, S3 = S + + #============================================================= + # integrate key + #============================================================= + """) + for i in irange(18): + write(indent+1, """\ + p%(i)d = P[%(i)d] ^ key_words[%(i)d] + """, i=i) + write(indent+1, """\ + + #============================================================= + # update P + #============================================================= + + #------------------------------------------------ + # update P[0] and P[1] + #------------------------------------------------ + l, r = p0, 0 + + """) + + render_encipher(write, indent+1) + + write(indent+1, """\ + + p0, p1 = l, r = r ^ p17, l + + """) + + for i in irange(2, 18, 2): + write(indent+1, """\ + #------------------------------------------------ + # update P[%(i)d] and P[%(i1)d] + #------------------------------------------------ + l ^= p0 + + """, i=i, i1=i+1) + + render_encipher(write, indent+1) + + write(indent+1, """\ + p%(i)d, p%(i1)d = l, r = r ^ p17, l + + """, i=i, i1=i+1) + + write(indent+1, """\ + + #------------------------------------------------ + # save changes to original P array + #------------------------------------------------ + P[:] = (p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, + p10, p11, p12, p13, p14, p15, p16, p17) + + #============================================================= + # update S + #============================================================= + + for box in S: + j = 0 + while j < 256: + l ^= p0 + + """) + + render_encipher(write, indent+3) + + write(indent+3, """\ + + box[j], box[j+1] = l, r = r ^ p17, l + j += 2 + """) + +#============================================================================= +# main +#============================================================================= + +def main(): + target = os.path.join(os.path.dirname(__file__), "unrolled.py") + fh = file(target, "w") + + def write(indent, msg, **kwds): + literal = kwds.pop("literal", False) + if kwds: + msg %= kwds + if not literal: + msg = textwrap.dedent(msg.rstrip(" ")) + if indent: + msg = indent_block(msg, " " * (indent*4)) + fh.write(msg) + + write(0, """\ + \"""passlib.crypto._blowfish.unrolled - unrolled loop implementation of bcrypt, + autogenerated by _gen_files.py + + currently this override the encipher() and expand() methods + with optimized versions, and leaves the other base.py methods alone. + \""" + #================================================================= + # imports + #================================================================= + # pkg + from passlib.crypto._blowfish.base import BlowfishEngine as _BlowfishEngine + # local + __all__ = [ + "BlowfishEngine", + ] + #================================================================= + # + #================================================================= + class BlowfishEngine(_BlowfishEngine): + + """) + + write_encipher_function(write, indent=1) + write_expand_function(write, indent=1) + + write(0, """\ + #================================================================= + # eoc + #================================================================= + + #================================================================= + # eof + #================================================================= + """) + +if __name__ == "__main__": + main() + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/crypto/_blowfish/base.py b/ansible/lib/python3.11/site-packages/passlib/crypto/_blowfish/base.py new file mode 100644 index 000000000..7b4f2cb4c --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/crypto/_blowfish/base.py @@ -0,0 +1,441 @@ +"""passlib.crypto._blowfish.base - unoptimized pure-python blowfish engine""" +#============================================================================= +# imports +#============================================================================= +# core +import struct +# pkg +from passlib.utils import repeat_string +# local +__all__ = [ + "BlowfishEngine", +] + +#============================================================================= +# blowfish constants +#============================================================================= +BLOWFISH_P = BLOWFISH_S = None + +def _init_constants(): + global BLOWFISH_P, BLOWFISH_S + + # NOTE: blowfish's spec states these numbers are the hex representation + # of the fractional portion of PI, in order. + + # Initial contents of key schedule - 18 integers + BLOWFISH_P = [ + 0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344, + 0xa4093822, 0x299f31d0, 0x082efa98, 0xec4e6c89, + 0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c, + 0xc0ac29b7, 0xc97c50dd, 0x3f84d5b5, 0xb5470917, + 0x9216d5d9, 0x8979fb1b, + ] + + # all 4 blowfish S boxes in one array - 256 integers per S box + BLOWFISH_S = [ + # sbox 1 + [ + 0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7, + 0xb8e1afed, 0x6a267e96, 0xba7c9045, 0xf12c7f99, + 0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16, + 0x636920d8, 0x71574e69, 0xa458fea3, 0xf4933d7e, + 0x0d95748f, 0x728eb658, 0x718bcd58, 0x82154aee, + 0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013, + 0xc5d1b023, 0x286085f0, 0xca417918, 0xb8db38ef, + 0x8e79dcb0, 0x603a180e, 0x6c9e0e8b, 0xb01e8a3e, + 0xd71577c1, 0xbd314b27, 0x78af2fda, 0x55605c60, + 0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440, + 0x55ca396a, 0x2aab10b6, 0xb4cc5c34, 0x1141e8ce, + 0xa15486af, 0x7c72e993, 0xb3ee1411, 0x636fbc2a, + 0x2ba9c55d, 0x741831f6, 0xce5c3e16, 0x9b87931e, + 0xafd6ba33, 0x6c24cf5c, 0x7a325381, 0x28958677, + 0x3b8f4898, 0x6b4bb9af, 0xc4bfe81b, 0x66282193, + 0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032, + 0xef845d5d, 0xe98575b1, 0xdc262302, 0xeb651b88, + 0x23893e81, 0xd396acc5, 0x0f6d6ff3, 0x83f44239, + 0x2e0b4482, 0xa4842004, 0x69c8f04a, 0x9e1f9b5e, + 0x21c66842, 0xf6e96c9a, 0x670c9c61, 0xabd388f0, + 0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3, + 0x6eef0b6c, 0x137a3be4, 0xba3bf050, 0x7efb2a98, + 0xa1f1651d, 0x39af0176, 0x66ca593e, 0x82430e88, + 0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 0x3b8b5ebe, + 0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6, + 0x4ed3aa62, 0x363f7706, 0x1bfedf72, 0x429b023d, + 0x37d0d724, 0xd00a1248, 0xdb0fead3, 0x49f1c09b, + 0x075372c9, 0x80991b7b, 0x25d479d8, 0xf6e8def7, + 0xe3fe501a, 0xb6794c3b, 0x976ce0bd, 0x04c006ba, + 0xc1a94fb6, 0x409f60c4, 0x5e5c9ec2, 0x196a2463, + 0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f, + 0x6dfc511f, 0x9b30952c, 0xcc814544, 0xaf5ebd09, + 0xbee3d004, 0xde334afd, 0x660f2807, 0x192e4bb3, + 0xc0cba857, 0x45c8740f, 0xd20b5f39, 0xb9d3fbdb, + 0x5579c0bd, 0x1a60320a, 0xd6a100c6, 0x402c7279, + 0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8, + 0x3c7516df, 0xfd616b15, 0x2f501ec8, 0xad0552ab, + 0x323db5fa, 0xfd238760, 0x53317b48, 0x3e00df82, + 0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 0xdf1769db, + 0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573, + 0x695b27b0, 0xbbca58c8, 0xe1ffa35d, 0xb8f011a0, + 0x10fa3d98, 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b, + 0x9a53e479, 0xb6f84565, 0xd28e49bc, 0x4bfb9790, + 0xe1ddf2da, 0xa4cb7e33, 0x62fb1341, 0xcee4c6e8, + 0xef20cada, 0x36774c01, 0xd07e9efe, 0x2bf11fb4, + 0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0, + 0xd08ed1d0, 0xafc725e0, 0x8e3c5b2f, 0x8e7594b7, + 0x8ff6e2fb, 0xf2122b64, 0x8888b812, 0x900df01c, + 0x4fad5ea0, 0x688fc31c, 0xd1cff191, 0xb3a8c1ad, + 0x2f2f2218, 0xbe0e1777, 0xea752dfe, 0x8b021fa1, + 0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299, + 0xb4a84fe0, 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9, + 0x165fa266, 0x80957705, 0x93cc7314, 0x211a1477, + 0xe6ad2065, 0x77b5fa86, 0xc75442f5, 0xfb9d35cf, + 0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49, + 0x00250e2d, 0x2071b35e, 0x226800bb, 0x57b8e0af, + 0x2464369b, 0xf009b91e, 0x5563911d, 0x59dfa6aa, + 0x78c14389, 0xd95a537f, 0x207d5ba2, 0x02e5b9c5, + 0x83260376, 0x6295cfa9, 0x11c81968, 0x4e734a41, + 0xb3472dca, 0x7b14a94a, 0x1b510052, 0x9a532915, + 0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400, + 0x08ba6fb5, 0x571be91f, 0xf296ec6b, 0x2a0dd915, + 0xb6636521, 0xe7b9f9b6, 0xff34052e, 0xc5855664, + 0x53b02d5d, 0xa99f8fa1, 0x08ba4799, 0x6e85076a, + ], + # sbox 2 + [ + 0x4b7a70e9, 0xb5b32944, 0xdb75092e, 0xc4192623, + 0xad6ea6b0, 0x49a7df7d, 0x9cee60b8, 0x8fedb266, + 0xecaa8c71, 0x699a17ff, 0x5664526c, 0xc2b19ee1, + 0x193602a5, 0x75094c29, 0xa0591340, 0xe4183a3e, + 0x3f54989a, 0x5b429d65, 0x6b8fe4d6, 0x99f73fd6, + 0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1, + 0x4cdd2086, 0x8470eb26, 0x6382e9c6, 0x021ecc5e, + 0x09686b3f, 0x3ebaefc9, 0x3c971814, 0x6b6a70a1, + 0x687f3584, 0x52a0e286, 0xb79c5305, 0xaa500737, + 0x3e07841c, 0x7fdeae5c, 0x8e7d44ec, 0x5716f2b8, + 0xb03ada37, 0xf0500c0d, 0xf01c1f04, 0x0200b3ff, + 0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd, + 0xd19113f9, 0x7ca92ff6, 0x94324773, 0x22f54701, + 0x3ae5e581, 0x37c2dadc, 0xc8b57634, 0x9af3dda7, + 0xa9446146, 0x0fd0030e, 0xecc8c73e, 0xa4751e41, + 0xe238cd99, 0x3bea0e2f, 0x3280bba1, 0x183eb331, + 0x4e548b38, 0x4f6db908, 0x6f420d03, 0xf60a04bf, + 0x2cb81290, 0x24977c79, 0x5679b072, 0xbcaf89af, + 0xde9a771f, 0xd9930810, 0xb38bae12, 0xdccf3f2e, + 0x5512721f, 0x2e6b7124, 0x501adde6, 0x9f84cd87, + 0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c, + 0xec7aec3a, 0xdb851dfa, 0x63094366, 0xc464c3d2, + 0xef1c1847, 0x3215d908, 0xdd433b37, 0x24c2ba16, + 0x12a14d43, 0x2a65c451, 0x50940002, 0x133ae4dd, + 0x71dff89e, 0x10314e55, 0x81ac77d6, 0x5f11199b, + 0x043556f1, 0xd7a3c76b, 0x3c11183b, 0x5924a509, + 0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e, + 0x86e34570, 0xeae96fb1, 0x860e5e0a, 0x5a3e2ab3, + 0x771fe71c, 0x4e3d06fa, 0x2965dcb9, 0x99e71d0f, + 0x803e89d6, 0x5266c825, 0x2e4cc978, 0x9c10b36a, + 0xc6150eba, 0x94e2ea78, 0xa5fc3c53, 0x1e0a2df4, + 0xf2f74ea7, 0x361d2b3d, 0x1939260f, 0x19c27960, + 0x5223a708, 0xf71312b6, 0xebadfe6e, 0xeac31f66, + 0xe3bc4595, 0xa67bc883, 0xb17f37d1, 0x018cff28, + 0xc332ddef, 0xbe6c5aa5, 0x65582185, 0x68ab9802, + 0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84, + 0x1521b628, 0x29076170, 0xecdd4775, 0x619f1510, + 0x13cca830, 0xeb61bd96, 0x0334fe1e, 0xaa0363cf, + 0xb5735c90, 0x4c70a239, 0xd59e9e0b, 0xcbaade14, + 0xeecc86bc, 0x60622ca7, 0x9cab5cab, 0xb2f3846e, + 0x648b1eaf, 0x19bdf0ca, 0xa02369b9, 0x655abb50, + 0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7, + 0x9b540b19, 0x875fa099, 0x95f7997e, 0x623d7da8, + 0xf837889a, 0x97e32d77, 0x11ed935f, 0x16681281, + 0x0e358829, 0xc7e61fd6, 0x96dedfa1, 0x7858ba99, + 0x57f584a5, 0x1b227263, 0x9b83c3ff, 0x1ac24696, + 0xcdb30aeb, 0x532e3054, 0x8fd948e4, 0x6dbc3128, + 0x58ebf2ef, 0x34c6ffea, 0xfe28ed61, 0xee7c3c73, + 0x5d4a14d9, 0xe864b7e3, 0x42105d14, 0x203e13e0, + 0x45eee2b6, 0xa3aaabea, 0xdb6c4f15, 0xfacb4fd0, + 0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105, + 0xd81e799e, 0x86854dc7, 0xe44b476a, 0x3d816250, + 0xcf62a1f2, 0x5b8d2646, 0xfc8883a0, 0xc1c7b6a3, + 0x7f1524c3, 0x69cb7492, 0x47848a0b, 0x5692b285, + 0x095bbf00, 0xad19489d, 0x1462b174, 0x23820e00, + 0x58428d2a, 0x0c55f5ea, 0x1dadf43e, 0x233f7061, + 0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb, + 0x7cde3759, 0xcbee7460, 0x4085f2a7, 0xce77326e, + 0xa6078084, 0x19f8509e, 0xe8efd855, 0x61d99735, + 0xa969a7aa, 0xc50c06c2, 0x5a04abfc, 0x800bcadc, + 0x9e447a2e, 0xc3453484, 0xfdd56705, 0x0e1e9ec9, + 0xdb73dbd3, 0x105588cd, 0x675fda79, 0xe3674340, + 0xc5c43465, 0x713e38d8, 0x3d28f89e, 0xf16dff20, + 0x153e21e7, 0x8fb03d4a, 0xe6e39f2b, 0xdb83adf7, + ], + # sbox 3 + [ + 0xe93d5a68, 0x948140f7, 0xf64c261c, 0x94692934, + 0x411520f7, 0x7602d4f7, 0xbcf46b2e, 0xd4a20068, + 0xd4082471, 0x3320f46a, 0x43b7d4b7, 0x500061af, + 0x1e39f62e, 0x97244546, 0x14214f74, 0xbf8b8840, + 0x4d95fc1d, 0x96b591af, 0x70f4ddd3, 0x66a02f45, + 0xbfbc09ec, 0x03bd9785, 0x7fac6dd0, 0x31cb8504, + 0x96eb27b3, 0x55fd3941, 0xda2547e6, 0xabca0a9a, + 0x28507825, 0x530429f4, 0x0a2c86da, 0xe9b66dfb, + 0x68dc1462, 0xd7486900, 0x680ec0a4, 0x27a18dee, + 0x4f3ffea2, 0xe887ad8c, 0xb58ce006, 0x7af4d6b6, + 0xaace1e7c, 0xd3375fec, 0xce78a399, 0x406b2a42, + 0x20fe9e35, 0xd9f385b9, 0xee39d7ab, 0x3b124e8b, + 0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2, + 0x3a6efa74, 0xdd5b4332, 0x6841e7f7, 0xca7820fb, + 0xfb0af54e, 0xd8feb397, 0x454056ac, 0xba489527, + 0x55533a3a, 0x20838d87, 0xfe6ba9b7, 0xd096954b, + 0x55a867bc, 0xa1159a58, 0xcca92963, 0x99e1db33, + 0xa62a4a56, 0x3f3125f9, 0x5ef47e1c, 0x9029317c, + 0xfdf8e802, 0x04272f70, 0x80bb155c, 0x05282ce3, + 0x95c11548, 0xe4c66d22, 0x48c1133f, 0xc70f86dc, + 0x07f9c9ee, 0x41041f0f, 0x404779a4, 0x5d886e17, + 0x325f51eb, 0xd59bc0d1, 0xf2bcc18f, 0x41113564, + 0x257b7834, 0x602a9c60, 0xdff8e8a3, 0x1f636c1b, + 0x0e12b4c2, 0x02e1329e, 0xaf664fd1, 0xcad18115, + 0x6b2395e0, 0x333e92e1, 0x3b240b62, 0xeebeb922, + 0x85b2a20e, 0xe6ba0d99, 0xde720c8c, 0x2da2f728, + 0xd0127845, 0x95b794fd, 0x647d0862, 0xe7ccf5f0, + 0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e, + 0x0a476341, 0x992eff74, 0x3a6f6eab, 0xf4f8fd37, + 0xa812dc60, 0xa1ebddf8, 0x991be14c, 0xdb6e6b0d, + 0xc67b5510, 0x6d672c37, 0x2765d43b, 0xdcd0e804, + 0xf1290dc7, 0xcc00ffa3, 0xb5390f92, 0x690fed0b, + 0x667b9ffb, 0xcedb7d9c, 0xa091cf0b, 0xd9155ea3, + 0xbb132f88, 0x515bad24, 0x7b9479bf, 0x763bd6eb, + 0x37392eb3, 0xcc115979, 0x8026e297, 0xf42e312d, + 0x6842ada7, 0xc66a2b3b, 0x12754ccc, 0x782ef11c, + 0x6a124237, 0xb79251e7, 0x06a1bbe6, 0x4bfb6350, + 0x1a6b1018, 0x11caedfa, 0x3d25bdd8, 0xe2e1c3c9, + 0x44421659, 0x0a121386, 0xd90cec6e, 0xd5abea2a, + 0x64af674e, 0xda86a85f, 0xbebfe988, 0x64e4c3fe, + 0x9dbc8057, 0xf0f7c086, 0x60787bf8, 0x6003604d, + 0xd1fd8346, 0xf6381fb0, 0x7745ae04, 0xd736fccc, + 0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f, + 0x77a057be, 0xbde8ae24, 0x55464299, 0xbf582e61, + 0x4e58f48f, 0xf2ddfda2, 0xf474ef38, 0x8789bdc2, + 0x5366f9c3, 0xc8b38e74, 0xb475f255, 0x46fcd9b9, + 0x7aeb2661, 0x8b1ddf84, 0x846a0e79, 0x915f95e2, + 0x466e598e, 0x20b45770, 0x8cd55591, 0xc902de4c, + 0xb90bace1, 0xbb8205d0, 0x11a86248, 0x7574a99e, + 0xb77f19b6, 0xe0a9dc09, 0x662d09a1, 0xc4324633, + 0xe85a1f02, 0x09f0be8c, 0x4a99a025, 0x1d6efe10, + 0x1ab93d1d, 0x0ba5a4df, 0xa186f20f, 0x2868f169, + 0xdcb7da83, 0x573906fe, 0xa1e2ce9b, 0x4fcd7f52, + 0x50115e01, 0xa70683fa, 0xa002b5c4, 0x0de6d027, + 0x9af88c27, 0x773f8641, 0xc3604c06, 0x61a806b5, + 0xf0177a28, 0xc0f586e0, 0x006058aa, 0x30dc7d62, + 0x11e69ed7, 0x2338ea63, 0x53c2dd94, 0xc2c21634, + 0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76, + 0x6f05e409, 0x4b7c0188, 0x39720a3d, 0x7c927c24, + 0x86e3725f, 0x724d9db9, 0x1ac15bb4, 0xd39eb8fc, + 0xed545578, 0x08fca5b5, 0xd83d7cd3, 0x4dad0fc4, + 0x1e50ef5e, 0xb161e6f8, 0xa28514d9, 0x6c51133c, + 0x6fd5c7e7, 0x56e14ec4, 0x362abfce, 0xddc6c837, + 0xd79a3234, 0x92638212, 0x670efa8e, 0x406000e0, + ], + # sbox 4 + [ + 0x3a39ce37, 0xd3faf5cf, 0xabc27737, 0x5ac52d1b, + 0x5cb0679e, 0x4fa33742, 0xd3822740, 0x99bc9bbe, + 0xd5118e9d, 0xbf0f7315, 0xd62d1c7e, 0xc700c47b, + 0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4, + 0x5748ab2f, 0xbc946e79, 0xc6a376d2, 0x6549c2c8, + 0x530ff8ee, 0x468dde7d, 0xd5730a1d, 0x4cd04dc6, + 0x2939bbdb, 0xa9ba4650, 0xac9526e8, 0xbe5ee304, + 0xa1fad5f0, 0x6a2d519a, 0x63ef8ce2, 0x9a86ee22, + 0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4, + 0x83c061ba, 0x9be96a4d, 0x8fe51550, 0xba645bd6, + 0x2826a2f9, 0xa73a3ae1, 0x4ba99586, 0xef5562e9, + 0xc72fefd3, 0xf752f7da, 0x3f046f69, 0x77fa0a59, + 0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593, + 0xe990fd5a, 0x9e34d797, 0x2cf0b7d9, 0x022b8b51, + 0x96d5ac3a, 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28, + 0x1f9f25cf, 0xadf2b89b, 0x5ad6b472, 0x5a88f54c, + 0xe029ac71, 0xe019a5e6, 0x47b0acfd, 0xed93fa9b, + 0xe8d3c48d, 0x283b57cc, 0xf8d56629, 0x79132e28, + 0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c, + 0x15056dd4, 0x88f46dba, 0x03a16125, 0x0564f0bd, + 0xc3eb9e15, 0x3c9057a2, 0x97271aec, 0xa93a072a, + 0x1b3f6d9b, 0x1e6321f5, 0xf59c66fb, 0x26dcf319, + 0x7533d928, 0xb155fdf5, 0x03563482, 0x8aba3cbb, + 0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f, + 0x4de81751, 0x3830dc8e, 0x379d5862, 0x9320f991, + 0xea7a90c2, 0xfb3e7bce, 0x5121ce64, 0x774fbe32, + 0xa8b6e37e, 0xc3293d46, 0x48de5369, 0x6413e680, + 0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166, + 0xb39a460a, 0x6445c0dd, 0x586cdecf, 0x1c20c8ae, + 0x5bbef7dd, 0x1b588d40, 0xccd2017f, 0x6bb4e3bb, + 0xdda26a7e, 0x3a59ff45, 0x3e350a44, 0xbcb4cdd5, + 0x72eacea8, 0xfa6484bb, 0x8d6612ae, 0xbf3c6f47, + 0xd29be463, 0x542f5d9e, 0xaec2771b, 0xf64e6370, + 0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d, + 0x4040cb08, 0x4eb4e2cc, 0x34d2466a, 0x0115af84, + 0xe1b00428, 0x95983a1d, 0x06b89fb4, 0xce6ea048, + 0x6f3f3b82, 0x3520ab82, 0x011a1d4b, 0x277227f8, + 0x611560b1, 0xe7933fdc, 0xbb3a792b, 0x344525bd, + 0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9, + 0xe01cc87e, 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7, + 0x1a908749, 0xd44fbd9a, 0xd0dadecb, 0xd50ada38, + 0x0339c32a, 0xc6913667, 0x8df9317c, 0xe0b12b4f, + 0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c, + 0xbf97222c, 0x15e6fc2a, 0x0f91fc71, 0x9b941525, + 0xfae59361, 0xceb69ceb, 0xc2a86459, 0x12baa8d1, + 0xb6c1075e, 0xe3056a0c, 0x10d25065, 0xcb03a442, + 0xe0ec6e0e, 0x1698db3b, 0x4c98a0be, 0x3278e964, + 0x9f1f9532, 0xe0d392df, 0xd3a0342b, 0x8971f21e, + 0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8, + 0xdf359f8d, 0x9b992f2e, 0xe60b6f47, 0x0fe3f11d, + 0xe54cda54, 0x1edad891, 0xce6279cf, 0xcd3e7e6f, + 0x1618b166, 0xfd2c1d05, 0x848fd2c5, 0xf6fb2299, + 0xf523f357, 0xa6327623, 0x93a83531, 0x56cccd02, + 0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc, + 0xde966292, 0x81b949d0, 0x4c50901b, 0x71c65614, + 0xe6c6c7bd, 0x327a140a, 0x45e1d006, 0xc3f27b9a, + 0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 0x35bdd2f6, + 0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b, + 0x53113ec0, 0x1640e3d3, 0x38abbd60, 0x2547adf0, + 0xba38209c, 0xf746ce76, 0x77afa1c5, 0x20756060, + 0x85cbfe4e, 0x8ae88dd8, 0x7aaaf9b0, 0x4cf9aa7e, + 0x1948c25c, 0x02fb8a8c, 0x01c36ae4, 0xd6ebe1f9, + 0x90d4f869, 0xa65cdea0, 0x3f09252d, 0xc208e69f, + 0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6, + ] + ] + +#============================================================================= +# engine +#============================================================================= +class BlowfishEngine(object): + + def __init__(self): + if BLOWFISH_P is None: + _init_constants() + self.P = list(BLOWFISH_P) + self.S = [ list(box) for box in BLOWFISH_S ] + + #=================================================================== + # common helpers + #=================================================================== + @staticmethod + def key_to_words(data, size=18): + """convert data to tuple of 4-byte integers, repeating or + truncating data as needed to reach specified size""" + assert isinstance(data, bytes) + dlen = len(data) + if not dlen: + # return all zeros - original C code would just read the NUL after + # the password, so mimicing that behavior for this edge case. + return [0]*size + + # repeat data until it fills up 4*size bytes + data = repeat_string(data, size<<2) + + # unpack + return struct.unpack(">%dI" % (size,), data) + + #=================================================================== + # blowfish routines + #=================================================================== + def encipher(self, l, r): + """loop version of blowfish encipher routine""" + P, S = self.P, self.S + l ^= P[0] + i = 1 + while i < 17: + # Feistel substitution on left word + r = ((((S[0][l >> 24] + S[1][(l >> 16) & 0xff]) ^ S[2][(l >> 8) & 0xff]) + + S[3][l & 0xff]) & 0xffffffff) ^ P[i] ^ r + # swap vars so even rounds do Feistel substition on right word + l, r = r, l + i += 1 + return r ^ P[17], l + + # NOTE: decipher is same as above, just with reversed(P) instead. + + def expand(self, key_words): + """perform stock Blowfish keyschedule setup""" + assert len(key_words) >= 18, "key_words must be at least as large as P" + P, S, encipher = self.P, self.S, self.encipher + + i = 0 + while i < 18: + P[i] ^= key_words[i] + i += 1 + + i = l = r = 0 + while i < 18: + P[i], P[i+1] = l,r = encipher(l,r) + i += 2 + + for box in S: + i = 0 + while i < 256: + box[i], box[i+1] = l,r = encipher(l,r) + i += 2 + + #=================================================================== + # eks-blowfish routines + #=================================================================== + def eks_salted_expand(self, key_words, salt_words): + """perform EKS' salted version of Blowfish keyschedule setup""" + # NOTE: this is the same as expand(), except for the addition + # of the operations involving *salt_words*. + + assert len(key_words) >= 18, "key_words must be at least as large as P" + salt_size = len(salt_words) + assert salt_size, "salt_words must not be empty" + assert not salt_size & 1, "salt_words must have even length" + P, S, encipher = self.P, self.S, self.encipher + + i = 0 + while i < 18: + P[i] ^= key_words[i] + i += 1 + + s = i = l = r = 0 + while i < 18: + l ^= salt_words[s] + r ^= salt_words[s+1] + s += 2 + if s == salt_size: + s = 0 + P[i], P[i+1] = l,r = encipher(l,r) # next() + i += 2 + + for box in S: + i = 0 + while i < 256: + l ^= salt_words[s] + r ^= salt_words[s+1] + s += 2 + if s == salt_size: + s = 0 + box[i], box[i+1] = l,r = encipher(l,r) # next() + i += 2 + + def eks_repeated_expand(self, key_words, salt_words, rounds): + """perform rounds stage of EKS keyschedule setup""" + expand = self.expand + n = 0 + while n < rounds: + expand(key_words) + expand(salt_words) + n += 1 + + def repeat_encipher(self, l, r, count): + """repeatedly apply encipher operation to a block""" + encipher = self.encipher + n = 0 + while n < count: + l, r = encipher(l, r) + n += 1 + return l, r + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/crypto/_blowfish/unrolled.py b/ansible/lib/python3.11/site-packages/passlib/crypto/_blowfish/unrolled.py new file mode 100644 index 000000000..4acf6e119 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/crypto/_blowfish/unrolled.py @@ -0,0 +1,771 @@ +"""passlib.crypto._blowfish.unrolled - unrolled loop implementation of bcrypt, +autogenerated by _gen_files.py + +currently this override the encipher() and expand() methods +with optimized versions, and leaves the other base.py methods alone. +""" +#============================================================================= +# imports +#============================================================================= +# pkg +from passlib.crypto._blowfish.base import BlowfishEngine as _BlowfishEngine +# local +__all__ = [ + "BlowfishEngine", +] +#============================================================================= +# +#============================================================================= +class BlowfishEngine(_BlowfishEngine): + + def encipher(self, l, r): + """blowfish encipher a single 64-bit block encoded as two 32-bit ints""" + + (p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, + p10, p11, p12, p13, p14, p15, p16, p17) = self.P + S0, S1, S2, S3 = self.S + + l ^= p0 + + # Feistel substitution on left word (round 0) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p1 + + # Feistel substitution on right word (round 1) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p2 + # Feistel substitution on left word (round 2) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p3 + + # Feistel substitution on right word (round 3) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p4 + # Feistel substitution on left word (round 4) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p5 + + # Feistel substitution on right word (round 5) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p6 + # Feistel substitution on left word (round 6) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p7 + + # Feistel substitution on right word (round 7) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p8 + # Feistel substitution on left word (round 8) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p9 + + # Feistel substitution on right word (round 9) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p10 + # Feistel substitution on left word (round 10) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p11 + + # Feistel substitution on right word (round 11) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p12 + # Feistel substitution on left word (round 12) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p13 + + # Feistel substitution on right word (round 13) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p14 + # Feistel substitution on left word (round 14) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p15 + + # Feistel substitution on right word (round 15) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p16 + + return r ^ p17, l + + def expand(self, key_words): + """unrolled version of blowfish key expansion""" + ##assert len(key_words) >= 18, "size of key_words must be >= 18" + + P, S = self.P, self.S + S0, S1, S2, S3 = S + + #============================================================= + # integrate key + #============================================================= + p0 = P[0] ^ key_words[0] + p1 = P[1] ^ key_words[1] + p2 = P[2] ^ key_words[2] + p3 = P[3] ^ key_words[3] + p4 = P[4] ^ key_words[4] + p5 = P[5] ^ key_words[5] + p6 = P[6] ^ key_words[6] + p7 = P[7] ^ key_words[7] + p8 = P[8] ^ key_words[8] + p9 = P[9] ^ key_words[9] + p10 = P[10] ^ key_words[10] + p11 = P[11] ^ key_words[11] + p12 = P[12] ^ key_words[12] + p13 = P[13] ^ key_words[13] + p14 = P[14] ^ key_words[14] + p15 = P[15] ^ key_words[15] + p16 = P[16] ^ key_words[16] + p17 = P[17] ^ key_words[17] + + #============================================================= + # update P + #============================================================= + + #------------------------------------------------ + # update P[0] and P[1] + #------------------------------------------------ + l, r = p0, 0 + + # Feistel substitution on left word (round 0) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p1 + + # Feistel substitution on right word (round 1) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p2 + # Feistel substitution on left word (round 2) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p3 + + # Feistel substitution on right word (round 3) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p4 + # Feistel substitution on left word (round 4) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p5 + + # Feistel substitution on right word (round 5) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p6 + # Feistel substitution on left word (round 6) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p7 + + # Feistel substitution on right word (round 7) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p8 + # Feistel substitution on left word (round 8) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p9 + + # Feistel substitution on right word (round 9) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p10 + # Feistel substitution on left word (round 10) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p11 + + # Feistel substitution on right word (round 11) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p12 + # Feistel substitution on left word (round 12) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p13 + + # Feistel substitution on right word (round 13) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p14 + # Feistel substitution on left word (round 14) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p15 + + # Feistel substitution on right word (round 15) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p16 + + p0, p1 = l, r = r ^ p17, l + + #------------------------------------------------ + # update P[2] and P[3] + #------------------------------------------------ + l ^= p0 + + # Feistel substitution on left word (round 0) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p1 + + # Feistel substitution on right word (round 1) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p2 + # Feistel substitution on left word (round 2) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p3 + + # Feistel substitution on right word (round 3) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p4 + # Feistel substitution on left word (round 4) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p5 + + # Feistel substitution on right word (round 5) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p6 + # Feistel substitution on left word (round 6) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p7 + + # Feistel substitution on right word (round 7) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p8 + # Feistel substitution on left word (round 8) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p9 + + # Feistel substitution on right word (round 9) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p10 + # Feistel substitution on left word (round 10) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p11 + + # Feistel substitution on right word (round 11) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p12 + # Feistel substitution on left word (round 12) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p13 + + # Feistel substitution on right word (round 13) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p14 + # Feistel substitution on left word (round 14) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p15 + + # Feistel substitution on right word (round 15) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p16 + p2, p3 = l, r = r ^ p17, l + + #------------------------------------------------ + # update P[4] and P[5] + #------------------------------------------------ + l ^= p0 + + # Feistel substitution on left word (round 0) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p1 + + # Feistel substitution on right word (round 1) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p2 + # Feistel substitution on left word (round 2) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p3 + + # Feistel substitution on right word (round 3) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p4 + # Feistel substitution on left word (round 4) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p5 + + # Feistel substitution on right word (round 5) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p6 + # Feistel substitution on left word (round 6) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p7 + + # Feistel substitution on right word (round 7) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p8 + # Feistel substitution on left word (round 8) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p9 + + # Feistel substitution on right word (round 9) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p10 + # Feistel substitution on left word (round 10) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p11 + + # Feistel substitution on right word (round 11) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p12 + # Feistel substitution on left word (round 12) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p13 + + # Feistel substitution on right word (round 13) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p14 + # Feistel substitution on left word (round 14) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p15 + + # Feistel substitution on right word (round 15) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p16 + p4, p5 = l, r = r ^ p17, l + + #------------------------------------------------ + # update P[6] and P[7] + #------------------------------------------------ + l ^= p0 + + # Feistel substitution on left word (round 0) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p1 + + # Feistel substitution on right word (round 1) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p2 + # Feistel substitution on left word (round 2) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p3 + + # Feistel substitution on right word (round 3) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p4 + # Feistel substitution on left word (round 4) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p5 + + # Feistel substitution on right word (round 5) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p6 + # Feistel substitution on left word (round 6) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p7 + + # Feistel substitution on right word (round 7) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p8 + # Feistel substitution on left word (round 8) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p9 + + # Feistel substitution on right word (round 9) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p10 + # Feistel substitution on left word (round 10) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p11 + + # Feistel substitution on right word (round 11) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p12 + # Feistel substitution on left word (round 12) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p13 + + # Feistel substitution on right word (round 13) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p14 + # Feistel substitution on left word (round 14) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p15 + + # Feistel substitution on right word (round 15) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p16 + p6, p7 = l, r = r ^ p17, l + + #------------------------------------------------ + # update P[8] and P[9] + #------------------------------------------------ + l ^= p0 + + # Feistel substitution on left word (round 0) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p1 + + # Feistel substitution on right word (round 1) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p2 + # Feistel substitution on left word (round 2) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p3 + + # Feistel substitution on right word (round 3) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p4 + # Feistel substitution on left word (round 4) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p5 + + # Feistel substitution on right word (round 5) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p6 + # Feistel substitution on left word (round 6) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p7 + + # Feistel substitution on right word (round 7) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p8 + # Feistel substitution on left word (round 8) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p9 + + # Feistel substitution on right word (round 9) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p10 + # Feistel substitution on left word (round 10) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p11 + + # Feistel substitution on right word (round 11) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p12 + # Feistel substitution on left word (round 12) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p13 + + # Feistel substitution on right word (round 13) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p14 + # Feistel substitution on left word (round 14) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p15 + + # Feistel substitution on right word (round 15) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p16 + p8, p9 = l, r = r ^ p17, l + + #------------------------------------------------ + # update P[10] and P[11] + #------------------------------------------------ + l ^= p0 + + # Feistel substitution on left word (round 0) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p1 + + # Feistel substitution on right word (round 1) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p2 + # Feistel substitution on left word (round 2) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p3 + + # Feistel substitution on right word (round 3) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p4 + # Feistel substitution on left word (round 4) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p5 + + # Feistel substitution on right word (round 5) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p6 + # Feistel substitution on left word (round 6) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p7 + + # Feistel substitution on right word (round 7) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p8 + # Feistel substitution on left word (round 8) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p9 + + # Feistel substitution on right word (round 9) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p10 + # Feistel substitution on left word (round 10) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p11 + + # Feistel substitution on right word (round 11) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p12 + # Feistel substitution on left word (round 12) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p13 + + # Feistel substitution on right word (round 13) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p14 + # Feistel substitution on left word (round 14) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p15 + + # Feistel substitution on right word (round 15) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p16 + p10, p11 = l, r = r ^ p17, l + + #------------------------------------------------ + # update P[12] and P[13] + #------------------------------------------------ + l ^= p0 + + # Feistel substitution on left word (round 0) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p1 + + # Feistel substitution on right word (round 1) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p2 + # Feistel substitution on left word (round 2) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p3 + + # Feistel substitution on right word (round 3) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p4 + # Feistel substitution on left word (round 4) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p5 + + # Feistel substitution on right word (round 5) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p6 + # Feistel substitution on left word (round 6) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p7 + + # Feistel substitution on right word (round 7) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p8 + # Feistel substitution on left word (round 8) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p9 + + # Feistel substitution on right word (round 9) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p10 + # Feistel substitution on left word (round 10) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p11 + + # Feistel substitution on right word (round 11) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p12 + # Feistel substitution on left word (round 12) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p13 + + # Feistel substitution on right word (round 13) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p14 + # Feistel substitution on left word (round 14) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p15 + + # Feistel substitution on right word (round 15) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p16 + p12, p13 = l, r = r ^ p17, l + + #------------------------------------------------ + # update P[14] and P[15] + #------------------------------------------------ + l ^= p0 + + # Feistel substitution on left word (round 0) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p1 + + # Feistel substitution on right word (round 1) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p2 + # Feistel substitution on left word (round 2) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p3 + + # Feistel substitution on right word (round 3) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p4 + # Feistel substitution on left word (round 4) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p5 + + # Feistel substitution on right word (round 5) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p6 + # Feistel substitution on left word (round 6) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p7 + + # Feistel substitution on right word (round 7) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p8 + # Feistel substitution on left word (round 8) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p9 + + # Feistel substitution on right word (round 9) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p10 + # Feistel substitution on left word (round 10) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p11 + + # Feistel substitution on right word (round 11) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p12 + # Feistel substitution on left word (round 12) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p13 + + # Feistel substitution on right word (round 13) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p14 + # Feistel substitution on left word (round 14) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p15 + + # Feistel substitution on right word (round 15) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p16 + p14, p15 = l, r = r ^ p17, l + + #------------------------------------------------ + # update P[16] and P[17] + #------------------------------------------------ + l ^= p0 + + # Feistel substitution on left word (round 0) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p1 + + # Feistel substitution on right word (round 1) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p2 + # Feistel substitution on left word (round 2) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p3 + + # Feistel substitution on right word (round 3) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p4 + # Feistel substitution on left word (round 4) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p5 + + # Feistel substitution on right word (round 5) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p6 + # Feistel substitution on left word (round 6) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p7 + + # Feistel substitution on right word (round 7) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p8 + # Feistel substitution on left word (round 8) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p9 + + # Feistel substitution on right word (round 9) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p10 + # Feistel substitution on left word (round 10) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p11 + + # Feistel substitution on right word (round 11) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p12 + # Feistel substitution on left word (round 12) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p13 + + # Feistel substitution on right word (round 13) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p14 + # Feistel substitution on left word (round 14) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p15 + + # Feistel substitution on right word (round 15) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p16 + p16, p17 = l, r = r ^ p17, l + + + #------------------------------------------------ + # save changes to original P array + #------------------------------------------------ + P[:] = (p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, + p10, p11, p12, p13, p14, p15, p16, p17) + + #============================================================= + # update S + #============================================================= + + for box in S: + j = 0 + while j < 256: + l ^= p0 + + # Feistel substitution on left word (round 0) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p1 + + # Feistel substitution on right word (round 1) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p2 + # Feistel substitution on left word (round 2) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p3 + + # Feistel substitution on right word (round 3) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p4 + # Feistel substitution on left word (round 4) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p5 + + # Feistel substitution on right word (round 5) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p6 + # Feistel substitution on left word (round 6) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p7 + + # Feistel substitution on right word (round 7) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p8 + # Feistel substitution on left word (round 8) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p9 + + # Feistel substitution on right word (round 9) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p10 + # Feistel substitution on left word (round 10) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p11 + + # Feistel substitution on right word (round 11) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p12 + # Feistel substitution on left word (round 12) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p13 + + # Feistel substitution on right word (round 13) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p14 + # Feistel substitution on left word (round 14) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p15 + + # Feistel substitution on right word (round 15) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p16 + + box[j], box[j+1] = l, r = r ^ p17, l + j += 2 + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/crypto/_md4.py b/ansible/lib/python3.11/site-packages/passlib/crypto/_md4.py new file mode 100644 index 000000000..bdc211fa2 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/crypto/_md4.py @@ -0,0 +1,244 @@ +""" +passlib.crypto._md4 -- fallback implementation of MD4 + +Helper implementing insecure and obsolete md4 algorithm. +used for NTHASH format, which is also insecure and broken, +since it's just md4(password). + +Implementated based on rfc at http://www.faqs.org/rfcs/rfc1320.html + +.. note:: + + This shouldn't be imported directly, it's merely used conditionally + by ``passlib.crypto.lookup_hash()`` when a native implementation can't be found. +""" + +#============================================================================= +# imports +#============================================================================= +# core +from binascii import hexlify +import struct +# site +from passlib.utils.compat import bascii_to_str, irange, PY3 +# local +__all__ = ["md4"] + +#============================================================================= +# utils +#============================================================================= +def F(x,y,z): + return (x&y) | ((~x) & z) + +def G(x,y,z): + return (x&y) | (x&z) | (y&z) + +##def H(x,y,z): +## return x ^ y ^ z + +MASK_32 = 2**32-1 + +#============================================================================= +# main class +#============================================================================= +class md4(object): + """pep-247 compatible implementation of MD4 hash algorithm + + .. attribute:: digest_size + + size of md4 digest in bytes (16 bytes) + + .. method:: update + + update digest by appending additional content + + .. method:: copy + + create clone of digest object, including current state + + .. method:: digest + + return bytes representing md4 digest of current content + + .. method:: hexdigest + + return hexadecimal version of digest + """ + # FIXME: make this follow hash object PEP better. + # FIXME: this isn't threadsafe + + name = "md4" + digest_size = digestsize = 16 + block_size = 64 + + _count = 0 # number of 64-byte blocks processed so far (not including _buf) + _state = None # list of [a,b,c,d] 32 bit ints used as internal register + _buf = None # data processed in 64 byte blocks, this holds leftover from last update + + def __init__(self, content=None): + self._count = 0 + self._state = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476] + self._buf = b'' + if content: + self.update(content) + + # round 1 table - [abcd k s] + _round1 = [ + [0,1,2,3, 0,3], + [3,0,1,2, 1,7], + [2,3,0,1, 2,11], + [1,2,3,0, 3,19], + + [0,1,2,3, 4,3], + [3,0,1,2, 5,7], + [2,3,0,1, 6,11], + [1,2,3,0, 7,19], + + [0,1,2,3, 8,3], + [3,0,1,2, 9,7], + [2,3,0,1, 10,11], + [1,2,3,0, 11,19], + + [0,1,2,3, 12,3], + [3,0,1,2, 13,7], + [2,3,0,1, 14,11], + [1,2,3,0, 15,19], + ] + + # round 2 table - [abcd k s] + _round2 = [ + [0,1,2,3, 0,3], + [3,0,1,2, 4,5], + [2,3,0,1, 8,9], + [1,2,3,0, 12,13], + + [0,1,2,3, 1,3], + [3,0,1,2, 5,5], + [2,3,0,1, 9,9], + [1,2,3,0, 13,13], + + [0,1,2,3, 2,3], + [3,0,1,2, 6,5], + [2,3,0,1, 10,9], + [1,2,3,0, 14,13], + + [0,1,2,3, 3,3], + [3,0,1,2, 7,5], + [2,3,0,1, 11,9], + [1,2,3,0, 15,13], + ] + + # round 3 table - [abcd k s] + _round3 = [ + [0,1,2,3, 0,3], + [3,0,1,2, 8,9], + [2,3,0,1, 4,11], + [1,2,3,0, 12,15], + + [0,1,2,3, 2,3], + [3,0,1,2, 10,9], + [2,3,0,1, 6,11], + [1,2,3,0, 14,15], + + [0,1,2,3, 1,3], + [3,0,1,2, 9,9], + [2,3,0,1, 5,11], + [1,2,3,0, 13,15], + + [0,1,2,3, 3,3], + [3,0,1,2, 11,9], + [2,3,0,1, 7,11], + [1,2,3,0, 15,15], + ] + + def _process(self, block): + """process 64 byte block""" + # unpack block into 16 32-bit ints + X = struct.unpack("<16I", block) + + # clone state + orig = self._state + state = list(orig) + + # round 1 - F function - (x&y)|(~x & z) + for a,b,c,d,k,s in self._round1: + t = (state[a] + F(state[b],state[c],state[d]) + X[k]) & MASK_32 + state[a] = ((t<>(32-s)) + + # round 2 - G function + for a,b,c,d,k,s in self._round2: + t = (state[a] + G(state[b],state[c],state[d]) + X[k] + 0x5a827999) & MASK_32 + state[a] = ((t<>(32-s)) + + # round 3 - H function - x ^ y ^ z + for a,b,c,d,k,s in self._round3: + t = (state[a] + (state[b] ^ state[c] ^ state[d]) + X[k] + 0x6ed9eba1) & MASK_32 + state[a] = ((t<>(32-s)) + + # add back into original state + for i in irange(4): + orig[i] = (orig[i]+state[i]) & MASK_32 + + def update(self, content): + if not isinstance(content, bytes): + if PY3: + raise TypeError("expected bytes") + else: + # replicate behavior of hashlib under py2 + content = content.encode("ascii") + buf = self._buf + if buf: + content = buf + content + idx = 0 + end = len(content) + while True: + next = idx + 64 + if next <= end: + self._process(content[idx:next]) + self._count += 1 + idx = next + else: + self._buf = content[idx:] + return + + def copy(self): + other = md4() + other._count = self._count + other._state = list(self._state) + other._buf = self._buf + return other + + def digest(self): + # NOTE: backing up state so we can restore it after _process is called, + # in case object is updated again (this is only attr altered by this method) + orig = list(self._state) + + # final block: buf + 0x80, + # then 0x00 padding until congruent w/ 56 mod 64 bytes + # then last 8 bytes = msg length in bits + buf = self._buf + msglen = self._count*512 + len(buf)*8 + block = buf + b'\x80' + b'\x00' * ((119-len(buf)) % 64) + \ + struct.pack("<2I", msglen & MASK_32, (msglen>>32) & MASK_32) + if len(block) == 128: + self._process(block[:64]) + self._process(block[64:]) + else: + assert len(block) == 64 + self._process(block) + + # render digest & restore un-finalized state + out = struct.pack("<4I", *self._state) + self._state = orig + return out + + def hexdigest(self): + return bascii_to_str(hexlify(self.digest())) + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/crypto/des.py b/ansible/lib/python3.11/site-packages/passlib/crypto/des.py new file mode 100644 index 000000000..3f87aef3b --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/crypto/des.py @@ -0,0 +1,848 @@ +"""passlib.crypto.des -- DES block encryption routines + +History +======= +These routines (which have since been drastically modified for python) +are based on a Java implementation of the des-crypt algorithm, +found at ``_. + +The copyright & license for that source is as follows:: + + UnixCrypt.java 0.9 96/11/25 + Copyright (c) 1996 Aki Yoshida. All rights reserved. + Permission to use, copy, modify and distribute this software + for non-commercial or commercial purposes and without fee is + hereby granted provided that this copyright notice appears in + all copies. + + --- + + Unix crypt(3C) utility + @version 0.9, 11/25/96 + @author Aki Yoshida + + --- + + modified April 2001 + by Iris Van den Broeke, Daniel Deville + + --- + Unix Crypt. + Implements the one way cryptography used by Unix systems for + simple password protection. + @version $Id: UnixCrypt2.txt,v 1.1.1.1 2005/09/13 22:20:13 christos Exp $ + @author Greg Wilkins (gregw) + +The netbsd des-crypt implementation has some nice notes on how this all works - + http://fxr.googlebit.com/source/lib/libcrypt/crypt.c?v=NETBSD-CURRENT +""" + +# TODO: could use an accelerated C version of this module to speed up lmhash, +# des-crypt, and ext-des-crypt + +#============================================================================= +# imports +#============================================================================= +# core +import struct +# pkg +from passlib import exc +from passlib.utils.compat import join_byte_values, byte_elem_value, \ + irange, irange, int_types +# local +__all__ = [ + "expand_des_key", + "des_encrypt_block", +] + +#============================================================================= +# constants +#============================================================================= + +# masks/upper limits for various integer sizes +INT_24_MASK = 0xffffff +INT_56_MASK = 0xffffffffffffff +INT_64_MASK = 0xffffffffffffffff + +# mask to clear parity bits from 64-bit key +_KDATA_MASK = 0xfefefefefefefefe +_KPARITY_MASK = 0x0101010101010101 + +# mask used to setup key schedule +_KS_MASK = 0xfcfcfcfcffffffff + +#============================================================================= +# static DES tables +#============================================================================= + +# placeholders filled in by _load_tables() +PCXROT = IE3264 = SPE = CF6464 = None + +def _load_tables(): + """delay loading tables until they are actually needed""" + global PCXROT, IE3264, SPE, CF6464 + + #--------------------------------------------------------------- + # Initial key schedule permutation + # PC1ROT - bit reverse, then PC1, then Rotate, then PC2 + #--------------------------------------------------------------- + # NOTE: this was reordered from original table to make perm3264 logic simpler + PC1ROT=( + ( 0x0000000000000000, 0x0000000000000000, 0x0000000000002000, 0x0000000000002000, + 0x0000000000000020, 0x0000000000000020, 0x0000000000002020, 0x0000000000002020, + 0x0000000000000400, 0x0000000000000400, 0x0000000000002400, 0x0000000000002400, + 0x0000000000000420, 0x0000000000000420, 0x0000000000002420, 0x0000000000002420, ), + ( 0x0000000000000000, 0x2000000000000000, 0x0000000400000000, 0x2000000400000000, + 0x0000800000000000, 0x2000800000000000, 0x0000800400000000, 0x2000800400000000, + 0x0008000000000000, 0x2008000000000000, 0x0008000400000000, 0x2008000400000000, + 0x0008800000000000, 0x2008800000000000, 0x0008800400000000, 0x2008800400000000, ), + ( 0x0000000000000000, 0x0000000000000000, 0x0000000000000040, 0x0000000000000040, + 0x0000000020000000, 0x0000000020000000, 0x0000000020000040, 0x0000000020000040, + 0x0000000000200000, 0x0000000000200000, 0x0000000000200040, 0x0000000000200040, + 0x0000000020200000, 0x0000000020200000, 0x0000000020200040, 0x0000000020200040, ), + ( 0x0000000000000000, 0x0002000000000000, 0x0800000000000000, 0x0802000000000000, + 0x0100000000000000, 0x0102000000000000, 0x0900000000000000, 0x0902000000000000, + 0x4000000000000000, 0x4002000000000000, 0x4800000000000000, 0x4802000000000000, + 0x4100000000000000, 0x4102000000000000, 0x4900000000000000, 0x4902000000000000, ), + ( 0x0000000000000000, 0x0000000000000000, 0x0000000000040000, 0x0000000000040000, + 0x0000020000000000, 0x0000020000000000, 0x0000020000040000, 0x0000020000040000, + 0x0000000000000004, 0x0000000000000004, 0x0000000000040004, 0x0000000000040004, + 0x0000020000000004, 0x0000020000000004, 0x0000020000040004, 0x0000020000040004, ), + ( 0x0000000000000000, 0x0000400000000000, 0x0200000000000000, 0x0200400000000000, + 0x0080000000000000, 0x0080400000000000, 0x0280000000000000, 0x0280400000000000, + 0x0000008000000000, 0x0000408000000000, 0x0200008000000000, 0x0200408000000000, + 0x0080008000000000, 0x0080408000000000, 0x0280008000000000, 0x0280408000000000, ), + ( 0x0000000000000000, 0x0000000000000000, 0x0000000010000000, 0x0000000010000000, + 0x0000000000001000, 0x0000000000001000, 0x0000000010001000, 0x0000000010001000, + 0x0000000040000000, 0x0000000040000000, 0x0000000050000000, 0x0000000050000000, + 0x0000000040001000, 0x0000000040001000, 0x0000000050001000, 0x0000000050001000, ), + ( 0x0000000000000000, 0x0000001000000000, 0x0000080000000000, 0x0000081000000000, + 0x1000000000000000, 0x1000001000000000, 0x1000080000000000, 0x1000081000000000, + 0x0004000000000000, 0x0004001000000000, 0x0004080000000000, 0x0004081000000000, + 0x1004000000000000, 0x1004001000000000, 0x1004080000000000, 0x1004081000000000, ), + ( 0x0000000000000000, 0x0000000000000000, 0x0000000000000080, 0x0000000000000080, + 0x0000000000080000, 0x0000000000080000, 0x0000000000080080, 0x0000000000080080, + 0x0000000000800000, 0x0000000000800000, 0x0000000000800080, 0x0000000000800080, + 0x0000000000880000, 0x0000000000880000, 0x0000000000880080, 0x0000000000880080, ), + ( 0x0000000000000000, 0x0000000008000000, 0x0000002000000000, 0x0000002008000000, + 0x0000100000000000, 0x0000100008000000, 0x0000102000000000, 0x0000102008000000, + 0x0000200000000000, 0x0000200008000000, 0x0000202000000000, 0x0000202008000000, + 0x0000300000000000, 0x0000300008000000, 0x0000302000000000, 0x0000302008000000, ), + ( 0x0000000000000000, 0x0000000000000000, 0x0000000000400000, 0x0000000000400000, + 0x0000000004000000, 0x0000000004000000, 0x0000000004400000, 0x0000000004400000, + 0x0000000000000800, 0x0000000000000800, 0x0000000000400800, 0x0000000000400800, + 0x0000000004000800, 0x0000000004000800, 0x0000000004400800, 0x0000000004400800, ), + ( 0x0000000000000000, 0x0000000000008000, 0x0040000000000000, 0x0040000000008000, + 0x0000004000000000, 0x0000004000008000, 0x0040004000000000, 0x0040004000008000, + 0x8000000000000000, 0x8000000000008000, 0x8040000000000000, 0x8040000000008000, + 0x8000004000000000, 0x8000004000008000, 0x8040004000000000, 0x8040004000008000, ), + ( 0x0000000000000000, 0x0000000000000000, 0x0000000000004000, 0x0000000000004000, + 0x0000000000000008, 0x0000000000000008, 0x0000000000004008, 0x0000000000004008, + 0x0000000000000010, 0x0000000000000010, 0x0000000000004010, 0x0000000000004010, + 0x0000000000000018, 0x0000000000000018, 0x0000000000004018, 0x0000000000004018, ), + ( 0x0000000000000000, 0x0000000200000000, 0x0001000000000000, 0x0001000200000000, + 0x0400000000000000, 0x0400000200000000, 0x0401000000000000, 0x0401000200000000, + 0x0020000000000000, 0x0020000200000000, 0x0021000000000000, 0x0021000200000000, + 0x0420000000000000, 0x0420000200000000, 0x0421000000000000, 0x0421000200000000, ), + ( 0x0000000000000000, 0x0000000000000000, 0x0000010000000000, 0x0000010000000000, + 0x0000000100000000, 0x0000000100000000, 0x0000010100000000, 0x0000010100000000, + 0x0000000000100000, 0x0000000000100000, 0x0000010000100000, 0x0000010000100000, + 0x0000000100100000, 0x0000000100100000, 0x0000010100100000, 0x0000010100100000, ), + ( 0x0000000000000000, 0x0000000080000000, 0x0000040000000000, 0x0000040080000000, + 0x0010000000000000, 0x0010000080000000, 0x0010040000000000, 0x0010040080000000, + 0x0000000800000000, 0x0000000880000000, 0x0000040800000000, 0x0000040880000000, + 0x0010000800000000, 0x0010000880000000, 0x0010040800000000, 0x0010040880000000, ), + ) + #--------------------------------------------------------------- + # Subsequent key schedule rotation permutations + # PC2ROT - PC2 inverse, then Rotate, then PC2 + #--------------------------------------------------------------- + # NOTE: this was reordered from original table to make perm3264 logic simpler + PC2ROTA=( + ( 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000200000, 0x0000000000200000, 0x0000000000200000, 0x0000000000200000, + 0x0000000004000000, 0x0000000004000000, 0x0000000004000000, 0x0000000004000000, + 0x0000000004200000, 0x0000000004200000, 0x0000000004200000, 0x0000000004200000, ), + ( 0x0000000000000000, 0x0000000000000800, 0x0000010000000000, 0x0000010000000800, + 0x0000000000002000, 0x0000000000002800, 0x0000010000002000, 0x0000010000002800, + 0x0000000010000000, 0x0000000010000800, 0x0000010010000000, 0x0000010010000800, + 0x0000000010002000, 0x0000000010002800, 0x0000010010002000, 0x0000010010002800, ), + ( 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000100000000, 0x0000000100000000, 0x0000000100000000, 0x0000000100000000, + 0x0000000000800000, 0x0000000000800000, 0x0000000000800000, 0x0000000000800000, + 0x0000000100800000, 0x0000000100800000, 0x0000000100800000, 0x0000000100800000, ), + ( 0x0000000000000000, 0x0000020000000000, 0x0000000080000000, 0x0000020080000000, + 0x0000000000400000, 0x0000020000400000, 0x0000000080400000, 0x0000020080400000, + 0x0000000008000000, 0x0000020008000000, 0x0000000088000000, 0x0000020088000000, + 0x0000000008400000, 0x0000020008400000, 0x0000000088400000, 0x0000020088400000, ), + ( 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000040, 0x0000000000000040, 0x0000000000000040, 0x0000000000000040, + 0x0000000000001000, 0x0000000000001000, 0x0000000000001000, 0x0000000000001000, + 0x0000000000001040, 0x0000000000001040, 0x0000000000001040, 0x0000000000001040, ), + ( 0x0000000000000000, 0x0000000000000010, 0x0000000000000400, 0x0000000000000410, + 0x0000000000000080, 0x0000000000000090, 0x0000000000000480, 0x0000000000000490, + 0x0000000040000000, 0x0000000040000010, 0x0000000040000400, 0x0000000040000410, + 0x0000000040000080, 0x0000000040000090, 0x0000000040000480, 0x0000000040000490, ), + ( 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000080000, 0x0000000000080000, 0x0000000000080000, 0x0000000000080000, + 0x0000000000100000, 0x0000000000100000, 0x0000000000100000, 0x0000000000100000, + 0x0000000000180000, 0x0000000000180000, 0x0000000000180000, 0x0000000000180000, ), + ( 0x0000000000000000, 0x0000000000040000, 0x0000000000000020, 0x0000000000040020, + 0x0000000000000004, 0x0000000000040004, 0x0000000000000024, 0x0000000000040024, + 0x0000000200000000, 0x0000000200040000, 0x0000000200000020, 0x0000000200040020, + 0x0000000200000004, 0x0000000200040004, 0x0000000200000024, 0x0000000200040024, ), + ( 0x0000000000000000, 0x0000000000000008, 0x0000000000008000, 0x0000000000008008, + 0x0010000000000000, 0x0010000000000008, 0x0010000000008000, 0x0010000000008008, + 0x0020000000000000, 0x0020000000000008, 0x0020000000008000, 0x0020000000008008, + 0x0030000000000000, 0x0030000000000008, 0x0030000000008000, 0x0030000000008008, ), + ( 0x0000000000000000, 0x0000400000000000, 0x0000080000000000, 0x0000480000000000, + 0x0000100000000000, 0x0000500000000000, 0x0000180000000000, 0x0000580000000000, + 0x4000000000000000, 0x4000400000000000, 0x4000080000000000, 0x4000480000000000, + 0x4000100000000000, 0x4000500000000000, 0x4000180000000000, 0x4000580000000000, ), + ( 0x0000000000000000, 0x0000000000004000, 0x0000000020000000, 0x0000000020004000, + 0x0001000000000000, 0x0001000000004000, 0x0001000020000000, 0x0001000020004000, + 0x0200000000000000, 0x0200000000004000, 0x0200000020000000, 0x0200000020004000, + 0x0201000000000000, 0x0201000000004000, 0x0201000020000000, 0x0201000020004000, ), + ( 0x0000000000000000, 0x1000000000000000, 0x0004000000000000, 0x1004000000000000, + 0x0002000000000000, 0x1002000000000000, 0x0006000000000000, 0x1006000000000000, + 0x0000000800000000, 0x1000000800000000, 0x0004000800000000, 0x1004000800000000, + 0x0002000800000000, 0x1002000800000000, 0x0006000800000000, 0x1006000800000000, ), + ( 0x0000000000000000, 0x0040000000000000, 0x2000000000000000, 0x2040000000000000, + 0x0000008000000000, 0x0040008000000000, 0x2000008000000000, 0x2040008000000000, + 0x0000001000000000, 0x0040001000000000, 0x2000001000000000, 0x2040001000000000, + 0x0000009000000000, 0x0040009000000000, 0x2000009000000000, 0x2040009000000000, ), + ( 0x0000000000000000, 0x0400000000000000, 0x8000000000000000, 0x8400000000000000, + 0x0000002000000000, 0x0400002000000000, 0x8000002000000000, 0x8400002000000000, + 0x0100000000000000, 0x0500000000000000, 0x8100000000000000, 0x8500000000000000, + 0x0100002000000000, 0x0500002000000000, 0x8100002000000000, 0x8500002000000000, ), + ( 0x0000000000000000, 0x0000800000000000, 0x0800000000000000, 0x0800800000000000, + 0x0000004000000000, 0x0000804000000000, 0x0800004000000000, 0x0800804000000000, + 0x0000000400000000, 0x0000800400000000, 0x0800000400000000, 0x0800800400000000, + 0x0000004400000000, 0x0000804400000000, 0x0800004400000000, 0x0800804400000000, ), + ( 0x0000000000000000, 0x0080000000000000, 0x0000040000000000, 0x0080040000000000, + 0x0008000000000000, 0x0088000000000000, 0x0008040000000000, 0x0088040000000000, + 0x0000200000000000, 0x0080200000000000, 0x0000240000000000, 0x0080240000000000, + 0x0008200000000000, 0x0088200000000000, 0x0008240000000000, 0x0088240000000000, ), + ) + + # NOTE: this was reordered from original table to make perm3264 logic simpler + PC2ROTB=( + ( 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000400, 0x0000000000000400, 0x0000000000000400, 0x0000000000000400, + 0x0000000000080000, 0x0000000000080000, 0x0000000000080000, 0x0000000000080000, + 0x0000000000080400, 0x0000000000080400, 0x0000000000080400, 0x0000000000080400, ), + ( 0x0000000000000000, 0x0000000000800000, 0x0000000000004000, 0x0000000000804000, + 0x0000000080000000, 0x0000000080800000, 0x0000000080004000, 0x0000000080804000, + 0x0000000000040000, 0x0000000000840000, 0x0000000000044000, 0x0000000000844000, + 0x0000000080040000, 0x0000000080840000, 0x0000000080044000, 0x0000000080844000, ), + ( 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000008, 0x0000000000000008, 0x0000000000000008, 0x0000000000000008, + 0x0000000040000000, 0x0000000040000000, 0x0000000040000000, 0x0000000040000000, + 0x0000000040000008, 0x0000000040000008, 0x0000000040000008, 0x0000000040000008, ), + ( 0x0000000000000000, 0x0000000020000000, 0x0000000200000000, 0x0000000220000000, + 0x0000000000000080, 0x0000000020000080, 0x0000000200000080, 0x0000000220000080, + 0x0000000000100000, 0x0000000020100000, 0x0000000200100000, 0x0000000220100000, + 0x0000000000100080, 0x0000000020100080, 0x0000000200100080, 0x0000000220100080, ), + ( 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000002000, 0x0000000000002000, 0x0000000000002000, 0x0000000000002000, + 0x0000020000000000, 0x0000020000000000, 0x0000020000000000, 0x0000020000000000, + 0x0000020000002000, 0x0000020000002000, 0x0000020000002000, 0x0000020000002000, ), + ( 0x0000000000000000, 0x0000000000000800, 0x0000000100000000, 0x0000000100000800, + 0x0000000010000000, 0x0000000010000800, 0x0000000110000000, 0x0000000110000800, + 0x0000000000000004, 0x0000000000000804, 0x0000000100000004, 0x0000000100000804, + 0x0000000010000004, 0x0000000010000804, 0x0000000110000004, 0x0000000110000804, ), + ( 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000001000, 0x0000000000001000, 0x0000000000001000, 0x0000000000001000, + 0x0000000000000010, 0x0000000000000010, 0x0000000000000010, 0x0000000000000010, + 0x0000000000001010, 0x0000000000001010, 0x0000000000001010, 0x0000000000001010, ), + ( 0x0000000000000000, 0x0000000000000040, 0x0000010000000000, 0x0000010000000040, + 0x0000000000200000, 0x0000000000200040, 0x0000010000200000, 0x0000010000200040, + 0x0000000000008000, 0x0000000000008040, 0x0000010000008000, 0x0000010000008040, + 0x0000000000208000, 0x0000000000208040, 0x0000010000208000, 0x0000010000208040, ), + ( 0x0000000000000000, 0x0000000004000000, 0x0000000008000000, 0x000000000c000000, + 0x0400000000000000, 0x0400000004000000, 0x0400000008000000, 0x040000000c000000, + 0x8000000000000000, 0x8000000004000000, 0x8000000008000000, 0x800000000c000000, + 0x8400000000000000, 0x8400000004000000, 0x8400000008000000, 0x840000000c000000, ), + ( 0x0000000000000000, 0x0002000000000000, 0x0200000000000000, 0x0202000000000000, + 0x1000000000000000, 0x1002000000000000, 0x1200000000000000, 0x1202000000000000, + 0x0008000000000000, 0x000a000000000000, 0x0208000000000000, 0x020a000000000000, + 0x1008000000000000, 0x100a000000000000, 0x1208000000000000, 0x120a000000000000, ), + ( 0x0000000000000000, 0x0000000000400000, 0x0000000000000020, 0x0000000000400020, + 0x0040000000000000, 0x0040000000400000, 0x0040000000000020, 0x0040000000400020, + 0x0800000000000000, 0x0800000000400000, 0x0800000000000020, 0x0800000000400020, + 0x0840000000000000, 0x0840000000400000, 0x0840000000000020, 0x0840000000400020, ), + ( 0x0000000000000000, 0x0080000000000000, 0x0000008000000000, 0x0080008000000000, + 0x2000000000000000, 0x2080000000000000, 0x2000008000000000, 0x2080008000000000, + 0x0020000000000000, 0x00a0000000000000, 0x0020008000000000, 0x00a0008000000000, + 0x2020000000000000, 0x20a0000000000000, 0x2020008000000000, 0x20a0008000000000, ), + ( 0x0000000000000000, 0x0000002000000000, 0x0000040000000000, 0x0000042000000000, + 0x4000000000000000, 0x4000002000000000, 0x4000040000000000, 0x4000042000000000, + 0x0000400000000000, 0x0000402000000000, 0x0000440000000000, 0x0000442000000000, + 0x4000400000000000, 0x4000402000000000, 0x4000440000000000, 0x4000442000000000, ), + ( 0x0000000000000000, 0x0000004000000000, 0x0000200000000000, 0x0000204000000000, + 0x0000080000000000, 0x0000084000000000, 0x0000280000000000, 0x0000284000000000, + 0x0000800000000000, 0x0000804000000000, 0x0000a00000000000, 0x0000a04000000000, + 0x0000880000000000, 0x0000884000000000, 0x0000a80000000000, 0x0000a84000000000, ), + ( 0x0000000000000000, 0x0000000800000000, 0x0000000400000000, 0x0000000c00000000, + 0x0000100000000000, 0x0000100800000000, 0x0000100400000000, 0x0000100c00000000, + 0x0010000000000000, 0x0010000800000000, 0x0010000400000000, 0x0010000c00000000, + 0x0010100000000000, 0x0010100800000000, 0x0010100400000000, 0x0010100c00000000, ), + ( 0x0000000000000000, 0x0100000000000000, 0x0001000000000000, 0x0101000000000000, + 0x0000001000000000, 0x0100001000000000, 0x0001001000000000, 0x0101001000000000, + 0x0004000000000000, 0x0104000000000000, 0x0005000000000000, 0x0105000000000000, + 0x0004001000000000, 0x0104001000000000, 0x0005001000000000, 0x0105001000000000, ), + ) + #--------------------------------------------------------------- + # PCXROT - PC1ROT, PC2ROTA, PC2ROTB listed in order + # of the PC1 rotation schedule, as used by des_setkey + #--------------------------------------------------------------- + ##ROTATES = (1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1) + ##PCXROT = ( + ## PC1ROT, PC2ROTA, PC2ROTB, PC2ROTB, + ## PC2ROTB, PC2ROTB, PC2ROTB, PC2ROTB, + ## PC2ROTA, PC2ROTB, PC2ROTB, PC2ROTB, + ## PC2ROTB, PC2ROTB, PC2ROTB, PC2ROTA, + ## ) + + # NOTE: modified PCXROT to contain entrys broken into pairs, + # to help generate them in format best used by encoder. + PCXROT = ( + (PC1ROT, PC2ROTA), (PC2ROTB, PC2ROTB), + (PC2ROTB, PC2ROTB), (PC2ROTB, PC2ROTB), + (PC2ROTA, PC2ROTB), (PC2ROTB, PC2ROTB), + (PC2ROTB, PC2ROTB), (PC2ROTB, PC2ROTA), + ) + + #--------------------------------------------------------------- + # Bit reverse, intial permupation, expantion + # Initial permutation/expansion table + #--------------------------------------------------------------- + # NOTE: this was reordered from original table to make perm3264 logic simpler + IE3264=( + ( 0x0000000000000000, 0x0000000000800800, 0x0000000000008008, 0x0000000000808808, + 0x0000008008000000, 0x0000008008800800, 0x0000008008008008, 0x0000008008808808, + 0x0000000080080000, 0x0000000080880800, 0x0000000080088008, 0x0000000080888808, + 0x0000008088080000, 0x0000008088880800, 0x0000008088088008, 0x0000008088888808, ), + ( 0x0000000000000000, 0x0080080000000000, 0x0000800800000000, 0x0080880800000000, + 0x0800000000000080, 0x0880080000000080, 0x0800800800000080, 0x0880880800000080, + 0x8008000000000000, 0x8088080000000000, 0x8008800800000000, 0x8088880800000000, + 0x8808000000000080, 0x8888080000000080, 0x8808800800000080, 0x8888880800000080, ), + ( 0x0000000000000000, 0x0000000000001000, 0x0000000000000010, 0x0000000000001010, + 0x0000000010000000, 0x0000000010001000, 0x0000000010000010, 0x0000000010001010, + 0x0000000000100000, 0x0000000000101000, 0x0000000000100010, 0x0000000000101010, + 0x0000000010100000, 0x0000000010101000, 0x0000000010100010, 0x0000000010101010, ), + ( 0x0000000000000000, 0x0000100000000000, 0x0000001000000000, 0x0000101000000000, + 0x1000000000000000, 0x1000100000000000, 0x1000001000000000, 0x1000101000000000, + 0x0010000000000000, 0x0010100000000000, 0x0010001000000000, 0x0010101000000000, + 0x1010000000000000, 0x1010100000000000, 0x1010001000000000, 0x1010101000000000, ), + ( 0x0000000000000000, 0x0000000000002000, 0x0000000000000020, 0x0000000000002020, + 0x0000000020000000, 0x0000000020002000, 0x0000000020000020, 0x0000000020002020, + 0x0000000000200000, 0x0000000000202000, 0x0000000000200020, 0x0000000000202020, + 0x0000000020200000, 0x0000000020202000, 0x0000000020200020, 0x0000000020202020, ), + ( 0x0000000000000000, 0x0000200000000000, 0x0000002000000000, 0x0000202000000000, + 0x2000000000000000, 0x2000200000000000, 0x2000002000000000, 0x2000202000000000, + 0x0020000000000000, 0x0020200000000000, 0x0020002000000000, 0x0020202000000000, + 0x2020000000000000, 0x2020200000000000, 0x2020002000000000, 0x2020202000000000, ), + ( 0x0000000000000000, 0x0000000000004004, 0x0400000000000040, 0x0400000000004044, + 0x0000000040040000, 0x0000000040044004, 0x0400000040040040, 0x0400000040044044, + 0x0000000000400400, 0x0000000000404404, 0x0400000000400440, 0x0400000000404444, + 0x0000000040440400, 0x0000000040444404, 0x0400000040440440, 0x0400000040444444, ), + ( 0x0000000000000000, 0x0000400400000000, 0x0000004004000000, 0x0000404404000000, + 0x4004000000000000, 0x4004400400000000, 0x4004004004000000, 0x4004404404000000, + 0x0040040000000000, 0x0040440400000000, 0x0040044004000000, 0x0040444404000000, + 0x4044040000000000, 0x4044440400000000, 0x4044044004000000, 0x4044444404000000, ), + ) + + #--------------------------------------------------------------- + # Table that combines the S, P, and E operations. + #--------------------------------------------------------------- + SPE=( + ( 0x0080088008200000, 0x0000008008000000, 0x0000000000200020, 0x0080088008200020, + 0x0000000000200000, 0x0080088008000020, 0x0000008008000020, 0x0000000000200020, + 0x0080088008000020, 0x0080088008200000, 0x0000008008200000, 0x0080080000000020, + 0x0080080000200020, 0x0000000000200000, 0x0000000000000000, 0x0000008008000020, + 0x0000008008000000, 0x0000000000000020, 0x0080080000200000, 0x0080088008000000, + 0x0080088008200020, 0x0000008008200000, 0x0080080000000020, 0x0080080000200000, + 0x0000000000000020, 0x0080080000000000, 0x0080088008000000, 0x0000008008200020, + 0x0080080000000000, 0x0080080000200020, 0x0000008008200020, 0x0000000000000000, + 0x0000000000000000, 0x0080088008200020, 0x0080080000200000, 0x0000008008000020, + 0x0080088008200000, 0x0000008008000000, 0x0080080000000020, 0x0080080000200000, + 0x0000008008200020, 0x0080080000000000, 0x0080088008000000, 0x0000000000200020, + 0x0080088008000020, 0x0000000000000020, 0x0000000000200020, 0x0000008008200000, + 0x0080088008200020, 0x0080088008000000, 0x0000008008200000, 0x0080080000200020, + 0x0000000000200000, 0x0080080000000020, 0x0000008008000020, 0x0000000000000000, + 0x0000008008000000, 0x0000000000200000, 0x0080080000200020, 0x0080088008200000, + 0x0000000000000020, 0x0000008008200020, 0x0080080000000000, 0x0080088008000020, ), + ( 0x1000800810004004, 0x0000000000000000, 0x0000800810000000, 0x0000000010004004, + 0x1000000000004004, 0x1000800800000000, 0x0000800800004004, 0x0000800810000000, + 0x0000800800000000, 0x1000000010004004, 0x1000000000000000, 0x0000800800004004, + 0x1000000010000000, 0x0000800810004004, 0x0000000010004004, 0x1000000000000000, + 0x0000000010000000, 0x1000800800004004, 0x1000000010004004, 0x0000800800000000, + 0x1000800810000000, 0x0000000000004004, 0x0000000000000000, 0x1000000010000000, + 0x1000800800004004, 0x1000800810000000, 0x0000800810004004, 0x1000000000004004, + 0x0000000000004004, 0x0000000010000000, 0x1000800800000000, 0x1000800810004004, + 0x1000000010000000, 0x0000800810004004, 0x0000800800004004, 0x1000800810000000, + 0x1000800810004004, 0x1000000010000000, 0x1000000000004004, 0x0000000000000000, + 0x0000000000004004, 0x1000800800000000, 0x0000000010000000, 0x1000000010004004, + 0x0000800800000000, 0x0000000000004004, 0x1000800810000000, 0x1000800800004004, + 0x0000800810004004, 0x0000800800000000, 0x0000000000000000, 0x1000000000004004, + 0x1000000000000000, 0x1000800810004004, 0x0000800810000000, 0x0000000010004004, + 0x1000000010004004, 0x0000000010000000, 0x1000800800000000, 0x0000800800004004, + 0x1000800800004004, 0x1000000000000000, 0x0000000010004004, 0x0000800810000000, ), + ( 0x0000000000400410, 0x0010004004400400, 0x0010000000000000, 0x0010000000400410, + 0x0000004004000010, 0x0000000000400400, 0x0010000000400410, 0x0010004004000000, + 0x0010000000400400, 0x0000004004000000, 0x0000004004400400, 0x0000000000000010, + 0x0010004004400410, 0x0010000000000010, 0x0000000000000010, 0x0000004004400410, + 0x0000000000000000, 0x0000004004000010, 0x0010004004400400, 0x0010000000000000, + 0x0010000000000010, 0x0010004004400410, 0x0000004004000000, 0x0000000000400410, + 0x0000004004400410, 0x0010000000400400, 0x0010004004000010, 0x0000004004400400, + 0x0010004004000000, 0x0000000000000000, 0x0000000000400400, 0x0010004004000010, + 0x0010004004400400, 0x0010000000000000, 0x0000000000000010, 0x0000004004000000, + 0x0010000000000010, 0x0000004004000010, 0x0000004004400400, 0x0010000000400410, + 0x0000000000000000, 0x0010004004400400, 0x0010004004000000, 0x0000004004400410, + 0x0000004004000010, 0x0000000000400400, 0x0010004004400410, 0x0000000000000010, + 0x0010004004000010, 0x0000000000400410, 0x0000000000400400, 0x0010004004400410, + 0x0000004004000000, 0x0010000000400400, 0x0010000000400410, 0x0010004004000000, + 0x0010000000400400, 0x0000000000000000, 0x0000004004400410, 0x0010000000000010, + 0x0000000000400410, 0x0010004004000010, 0x0010000000000000, 0x0000004004400400, ), + ( 0x0800100040040080, 0x0000100000001000, 0x0800000000000080, 0x0800100040041080, + 0x0000000000000000, 0x0000000040041000, 0x0800100000001080, 0x0800000040040080, + 0x0000100040041000, 0x0800000000001080, 0x0000000000001000, 0x0800100000000080, + 0x0800000000001080, 0x0800100040040080, 0x0000000040040000, 0x0000000000001000, + 0x0800000040041080, 0x0000100040040000, 0x0000100000000000, 0x0800000000000080, + 0x0000100040040000, 0x0800100000001080, 0x0000000040041000, 0x0000100000000000, + 0x0800100000000080, 0x0000000000000000, 0x0800000040040080, 0x0000100040041000, + 0x0000100000001000, 0x0800000040041080, 0x0800100040041080, 0x0000000040040000, + 0x0800000040041080, 0x0800100000000080, 0x0000000040040000, 0x0800000000001080, + 0x0000100040040000, 0x0000100000001000, 0x0800000000000080, 0x0000000040041000, + 0x0800100000001080, 0x0000000000000000, 0x0000100000000000, 0x0800000040040080, + 0x0000000000000000, 0x0800000040041080, 0x0000100040041000, 0x0000100000000000, + 0x0000000000001000, 0x0800100040041080, 0x0800100040040080, 0x0000000040040000, + 0x0800100040041080, 0x0800000000000080, 0x0000100000001000, 0x0800100040040080, + 0x0800000040040080, 0x0000100040040000, 0x0000000040041000, 0x0800100000001080, + 0x0800100000000080, 0x0000000000001000, 0x0800000000001080, 0x0000100040041000, ), + ( 0x0000000000800800, 0x0000001000000000, 0x0040040000000000, 0x2040041000800800, + 0x2000001000800800, 0x0040040000800800, 0x2040041000000000, 0x0000001000800800, + 0x0000001000000000, 0x2000000000000000, 0x2000000000800800, 0x0040041000000000, + 0x2040040000800800, 0x2000001000800800, 0x0040041000800800, 0x0000000000000000, + 0x0040041000000000, 0x0000000000800800, 0x2000001000000000, 0x2040040000000000, + 0x0040040000800800, 0x2040041000000000, 0x0000000000000000, 0x2000000000800800, + 0x2000000000000000, 0x2040040000800800, 0x2040041000800800, 0x2000001000000000, + 0x0000001000800800, 0x0040040000000000, 0x2040040000000000, 0x0040041000800800, + 0x0040041000800800, 0x2040040000800800, 0x2000001000000000, 0x0000001000800800, + 0x0000001000000000, 0x2000000000000000, 0x2000000000800800, 0x0040040000800800, + 0x0000000000800800, 0x0040041000000000, 0x2040041000800800, 0x0000000000000000, + 0x2040041000000000, 0x0000000000800800, 0x0040040000000000, 0x2000001000000000, + 0x2040040000800800, 0x0040040000000000, 0x0000000000000000, 0x2040041000800800, + 0x2000001000800800, 0x0040041000800800, 0x2040040000000000, 0x0000001000000000, + 0x0040041000000000, 0x2000001000800800, 0x0040040000800800, 0x2040040000000000, + 0x2000000000000000, 0x2040041000000000, 0x0000001000800800, 0x2000000000800800, ), + ( 0x4004000000008008, 0x4004000020000000, 0x0000000000000000, 0x0000200020008008, + 0x4004000020000000, 0x0000200000000000, 0x4004200000008008, 0x0000000020000000, + 0x4004200000000000, 0x4004200020008008, 0x0000200020000000, 0x0000000000008008, + 0x0000200000008008, 0x4004000000008008, 0x0000000020008008, 0x4004200020000000, + 0x0000000020000000, 0x4004200000008008, 0x4004000020008008, 0x0000000000000000, + 0x0000200000000000, 0x4004000000000000, 0x0000200020008008, 0x4004000020008008, + 0x4004200020008008, 0x0000000020008008, 0x0000000000008008, 0x4004200000000000, + 0x4004000000000000, 0x0000200020000000, 0x4004200020000000, 0x0000200000008008, + 0x4004200000000000, 0x0000000000008008, 0x0000200000008008, 0x4004200020000000, + 0x0000200020008008, 0x4004000020000000, 0x0000000000000000, 0x0000200000008008, + 0x0000000000008008, 0x0000200000000000, 0x4004000020008008, 0x0000000020000000, + 0x4004000020000000, 0x4004200020008008, 0x0000200020000000, 0x4004000000000000, + 0x4004200020008008, 0x0000200020000000, 0x0000000020000000, 0x4004200000008008, + 0x4004000000008008, 0x0000000020008008, 0x4004200020000000, 0x0000000000000000, + 0x0000200000000000, 0x4004000000008008, 0x4004200000008008, 0x0000200020008008, + 0x0000000020008008, 0x4004200000000000, 0x4004000000000000, 0x4004000020008008, ), + ( 0x0000400400000000, 0x0020000000000000, 0x0020000000100000, 0x0400000000100040, + 0x0420400400100040, 0x0400400400000040, 0x0020400400000000, 0x0000000000000000, + 0x0000000000100000, 0x0420000000100040, 0x0420000000000040, 0x0000400400100000, + 0x0400000000000040, 0x0020400400100000, 0x0000400400100000, 0x0420000000000040, + 0x0420000000100040, 0x0000400400000000, 0x0400400400000040, 0x0420400400100040, + 0x0000000000000000, 0x0020000000100000, 0x0400000000100040, 0x0020400400000000, + 0x0400400400100040, 0x0420400400000040, 0x0020400400100000, 0x0400000000000040, + 0x0420400400000040, 0x0400400400100040, 0x0020000000000000, 0x0000000000100000, + 0x0420400400000040, 0x0000400400100000, 0x0400400400100040, 0x0420000000000040, + 0x0000400400000000, 0x0020000000000000, 0x0000000000100000, 0x0400400400100040, + 0x0420000000100040, 0x0420400400000040, 0x0020400400000000, 0x0000000000000000, + 0x0020000000000000, 0x0400000000100040, 0x0400000000000040, 0x0020000000100000, + 0x0000000000000000, 0x0420000000100040, 0x0020000000100000, 0x0020400400000000, + 0x0420000000000040, 0x0000400400000000, 0x0420400400100040, 0x0000000000100000, + 0x0020400400100000, 0x0400000000000040, 0x0400400400000040, 0x0420400400100040, + 0x0400000000100040, 0x0020400400100000, 0x0000400400100000, 0x0400400400000040, ), + ( 0x8008000080082000, 0x0000002080082000, 0x8008002000000000, 0x0000000000000000, + 0x0000002000002000, 0x8008000080080000, 0x0000000080082000, 0x8008002080082000, + 0x8008000000000000, 0x0000000000002000, 0x0000002080080000, 0x8008002000000000, + 0x8008002080080000, 0x8008002000002000, 0x8008000000002000, 0x0000000080082000, + 0x0000002000000000, 0x8008002080080000, 0x8008000080080000, 0x0000002000002000, + 0x8008002080082000, 0x8008000000002000, 0x0000000000000000, 0x0000002080080000, + 0x0000000000002000, 0x0000000080080000, 0x8008002000002000, 0x8008000080082000, + 0x0000000080080000, 0x0000002000000000, 0x0000002080082000, 0x8008000000000000, + 0x0000000080080000, 0x0000002000000000, 0x8008000000002000, 0x8008002080082000, + 0x8008002000000000, 0x0000000000002000, 0x0000000000000000, 0x0000002080080000, + 0x8008000080082000, 0x8008002000002000, 0x0000002000002000, 0x8008000080080000, + 0x0000002080082000, 0x8008000000000000, 0x8008000080080000, 0x0000002000002000, + 0x8008002080082000, 0x0000000080080000, 0x0000000080082000, 0x8008000000002000, + 0x0000002080080000, 0x8008002000000000, 0x8008002000002000, 0x0000000080082000, + 0x8008000000000000, 0x0000002080082000, 0x8008002080080000, 0x0000000000000000, + 0x0000000000002000, 0x8008000080082000, 0x0000002000000000, 0x8008002080080000, ), + ) + + #--------------------------------------------------------------- + # compressed/interleaved => final permutation table + # Compression, final permutation, bit reverse + #--------------------------------------------------------------- + # NOTE: this was reordered from original table to make perm6464 logic simpler + CF6464=( + ( 0x0000000000000000, 0x0000002000000000, 0x0000200000000000, 0x0000202000000000, + 0x0020000000000000, 0x0020002000000000, 0x0020200000000000, 0x0020202000000000, + 0x2000000000000000, 0x2000002000000000, 0x2000200000000000, 0x2000202000000000, + 0x2020000000000000, 0x2020002000000000, 0x2020200000000000, 0x2020202000000000, ), + ( 0x0000000000000000, 0x0000000200000000, 0x0000020000000000, 0x0000020200000000, + 0x0002000000000000, 0x0002000200000000, 0x0002020000000000, 0x0002020200000000, + 0x0200000000000000, 0x0200000200000000, 0x0200020000000000, 0x0200020200000000, + 0x0202000000000000, 0x0202000200000000, 0x0202020000000000, 0x0202020200000000, ), + ( 0x0000000000000000, 0x0000000000000020, 0x0000000000002000, 0x0000000000002020, + 0x0000000000200000, 0x0000000000200020, 0x0000000000202000, 0x0000000000202020, + 0x0000000020000000, 0x0000000020000020, 0x0000000020002000, 0x0000000020002020, + 0x0000000020200000, 0x0000000020200020, 0x0000000020202000, 0x0000000020202020, ), + ( 0x0000000000000000, 0x0000000000000002, 0x0000000000000200, 0x0000000000000202, + 0x0000000000020000, 0x0000000000020002, 0x0000000000020200, 0x0000000000020202, + 0x0000000002000000, 0x0000000002000002, 0x0000000002000200, 0x0000000002000202, + 0x0000000002020000, 0x0000000002020002, 0x0000000002020200, 0x0000000002020202, ), + ( 0x0000000000000000, 0x0000008000000000, 0x0000800000000000, 0x0000808000000000, + 0x0080000000000000, 0x0080008000000000, 0x0080800000000000, 0x0080808000000000, + 0x8000000000000000, 0x8000008000000000, 0x8000800000000000, 0x8000808000000000, + 0x8080000000000000, 0x8080008000000000, 0x8080800000000000, 0x8080808000000000, ), + ( 0x0000000000000000, 0x0000000800000000, 0x0000080000000000, 0x0000080800000000, + 0x0008000000000000, 0x0008000800000000, 0x0008080000000000, 0x0008080800000000, + 0x0800000000000000, 0x0800000800000000, 0x0800080000000000, 0x0800080800000000, + 0x0808000000000000, 0x0808000800000000, 0x0808080000000000, 0x0808080800000000, ), + ( 0x0000000000000000, 0x0000000000000080, 0x0000000000008000, 0x0000000000008080, + 0x0000000000800000, 0x0000000000800080, 0x0000000000808000, 0x0000000000808080, + 0x0000000080000000, 0x0000000080000080, 0x0000000080008000, 0x0000000080008080, + 0x0000000080800000, 0x0000000080800080, 0x0000000080808000, 0x0000000080808080, ), + ( 0x0000000000000000, 0x0000000000000008, 0x0000000000000800, 0x0000000000000808, + 0x0000000000080000, 0x0000000000080008, 0x0000000000080800, 0x0000000000080808, + 0x0000000008000000, 0x0000000008000008, 0x0000000008000800, 0x0000000008000808, + 0x0000000008080000, 0x0000000008080008, 0x0000000008080800, 0x0000000008080808, ), + ( 0x0000000000000000, 0x0000001000000000, 0x0000100000000000, 0x0000101000000000, + 0x0010000000000000, 0x0010001000000000, 0x0010100000000000, 0x0010101000000000, + 0x1000000000000000, 0x1000001000000000, 0x1000100000000000, 0x1000101000000000, + 0x1010000000000000, 0x1010001000000000, 0x1010100000000000, 0x1010101000000000, ), + ( 0x0000000000000000, 0x0000000100000000, 0x0000010000000000, 0x0000010100000000, + 0x0001000000000000, 0x0001000100000000, 0x0001010000000000, 0x0001010100000000, + 0x0100000000000000, 0x0100000100000000, 0x0100010000000000, 0x0100010100000000, + 0x0101000000000000, 0x0101000100000000, 0x0101010000000000, 0x0101010100000000, ), + ( 0x0000000000000000, 0x0000000000000010, 0x0000000000001000, 0x0000000000001010, + 0x0000000000100000, 0x0000000000100010, 0x0000000000101000, 0x0000000000101010, + 0x0000000010000000, 0x0000000010000010, 0x0000000010001000, 0x0000000010001010, + 0x0000000010100000, 0x0000000010100010, 0x0000000010101000, 0x0000000010101010, ), + ( 0x0000000000000000, 0x0000000000000001, 0x0000000000000100, 0x0000000000000101, + 0x0000000000010000, 0x0000000000010001, 0x0000000000010100, 0x0000000000010101, + 0x0000000001000000, 0x0000000001000001, 0x0000000001000100, 0x0000000001000101, + 0x0000000001010000, 0x0000000001010001, 0x0000000001010100, 0x0000000001010101, ), + ( 0x0000000000000000, 0x0000004000000000, 0x0000400000000000, 0x0000404000000000, + 0x0040000000000000, 0x0040004000000000, 0x0040400000000000, 0x0040404000000000, + 0x4000000000000000, 0x4000004000000000, 0x4000400000000000, 0x4000404000000000, + 0x4040000000000000, 0x4040004000000000, 0x4040400000000000, 0x4040404000000000, ), + ( 0x0000000000000000, 0x0000000400000000, 0x0000040000000000, 0x0000040400000000, + 0x0004000000000000, 0x0004000400000000, 0x0004040000000000, 0x0004040400000000, + 0x0400000000000000, 0x0400000400000000, 0x0400040000000000, 0x0400040400000000, + 0x0404000000000000, 0x0404000400000000, 0x0404040000000000, 0x0404040400000000, ), + ( 0x0000000000000000, 0x0000000000000040, 0x0000000000004000, 0x0000000000004040, + 0x0000000000400000, 0x0000000000400040, 0x0000000000404000, 0x0000000000404040, + 0x0000000040000000, 0x0000000040000040, 0x0000000040004000, 0x0000000040004040, + 0x0000000040400000, 0x0000000040400040, 0x0000000040404000, 0x0000000040404040, ), + ( 0x0000000000000000, 0x0000000000000004, 0x0000000000000400, 0x0000000000000404, + 0x0000000000040000, 0x0000000000040004, 0x0000000000040400, 0x0000000000040404, + 0x0000000004000000, 0x0000000004000004, 0x0000000004000400, 0x0000000004000404, + 0x0000000004040000, 0x0000000004040004, 0x0000000004040400, 0x0000000004040404, ), + ) + #=================================================================== + # eof _load_tables() + #=================================================================== + +#============================================================================= +# support +#============================================================================= + +def _permute(c, p): + """Returns the permutation of the given 32-bit or 64-bit code with + the specified permutation table.""" + # NOTE: only difference between 32 & 64 bit permutations + # is that len(p)==8 for 32 bit, and len(p)==16 for 64 bit. + out = 0 + for r in p: + out |= r[c&0xf] + c >>= 4 + return out + +#============================================================================= +# packing & unpacking +#============================================================================= +# FIXME: more properly named _uint8_struct... +_uint64_struct = struct.Struct(">Q") + +def _pack64(value): + return _uint64_struct.pack(value) + +def _unpack64(value): + return _uint64_struct.unpack(value)[0] + +def _pack56(value): + return _uint64_struct.pack(value)[1:] + +def _unpack56(value): + return _uint64_struct.unpack(b'\x00' + value)[0] + +#============================================================================= +# 56->64 key manipulation +#============================================================================= + +##def expand_7bit(value): +## "expand 7-bit integer => 7-bits + 1 odd-parity bit" +## # parity calc adapted from 32-bit even parity alg found at +## # http://graphics.stanford.edu/~seander/bithacks.html#ParityParallel +## assert 0 <= value < 0x80, "value out of range" +## return (value<<1) | (0x9669 >> ((value ^ (value >> 4)) & 0xf)) & 1 + +_EXPAND_ITER = irange(49,-7,-7) + +def expand_des_key(key): + """convert DES from 7 bytes to 8 bytes (by inserting empty parity bits)""" + if isinstance(key, bytes): + if len(key) != 7: + raise ValueError("key must be 7 bytes in size") + elif isinstance(key, int_types): + if key < 0 or key > INT_56_MASK: + raise ValueError("key must be 56-bit non-negative integer") + return _unpack64(expand_des_key(_pack56(key))) + else: + raise exc.ExpectedTypeError(key, "bytes or int", "key") + key = _unpack56(key) + # NOTE: the following would insert correctly-valued parity bits in each key, + # but the parity bit would just be ignored in des_encrypt_block(), + # so not bothering to use it. + # XXX: could make parity-restoring optionally available via flag + ##return join_byte_values(expand_7bit((key >> shift) & 0x7f) + ## for shift in _EXPAND_ITER) + return join_byte_values(((key>>shift) & 0x7f)<<1 for shift in _EXPAND_ITER) + +def shrink_des_key(key): + """convert DES key from 8 bytes to 7 bytes (by discarding the parity bits)""" + if isinstance(key, bytes): + if len(key) != 8: + raise ValueError("key must be 8 bytes in size") + return _pack56(shrink_des_key(_unpack64(key))) + elif isinstance(key, int_types): + if key < 0 or key > INT_64_MASK: + raise ValueError("key must be 64-bit non-negative integer") + else: + raise exc.ExpectedTypeError(key, "bytes or int", "key") + key >>= 1 + result = 0 + offset = 0 + while offset < 56: + result |= (key & 0x7f)<>= 8 + offset += 7 + assert not (result & ~INT_64_MASK) + return result + +#============================================================================= +# des encryption +#============================================================================= +def des_encrypt_block(key, input, salt=0, rounds=1): + """encrypt single block of data using DES, operates on 8-byte strings. + + :arg key: + DES key as 7 byte string, or 8 byte string with parity bits + (parity bit values are ignored). + + :arg input: + plaintext block to encrypt, as 8 byte string. + + :arg salt: + Optional 24-bit integer used to mutate the base DES algorithm in a + manner specific to :class:`~passlib.hash.des_crypt` and its variants. + The default value ``0`` provides the normal (unsalted) DES behavior. + The salt functions as follows: + if the ``i``'th bit of ``salt`` is set, + bits ``i`` and ``i+24`` are swapped in the DES E-box output. + + :arg rounds: + Optional number of rounds of to apply the DES key schedule. + the default (``rounds=1``) provides the normal DES behavior, + but :class:`~passlib.hash.des_crypt` and its variants use + alternate rounds values. + + :raises TypeError: if any of the provided args are of the wrong type. + :raises ValueError: + if any of the input blocks are the wrong size, + or the salt/rounds values are out of range. + + :returns: + resulting 8-byte ciphertext block. + """ + # validate & unpack key + if isinstance(key, bytes): + if len(key) == 7: + key = expand_des_key(key) + elif len(key) != 8: + raise ValueError("key must be 7 or 8 bytes") + key = _unpack64(key) + else: + raise exc.ExpectedTypeError(key, "bytes", "key") + + # validate & unpack input + if isinstance(input, bytes): + if len(input) != 8: + raise ValueError("input block must be 8 bytes") + input = _unpack64(input) + else: + raise exc.ExpectedTypeError(input, "bytes", "input") + + # hand things off to other func + result = des_encrypt_int_block(key, input, salt, rounds) + + # repack result + return _pack64(result) + +def des_encrypt_int_block(key, input, salt=0, rounds=1): + """encrypt single block of data using DES, operates on 64-bit integers. + + this function is essentially the same as :func:`des_encrypt_block`, + except that it operates on integers, and will NOT automatically + expand 56-bit keys if provided (since there's no way to detect them). + + :arg key: + DES key as 64-bit integer (the parity bits are ignored). + + :arg input: + input block as 64-bit integer + + :arg salt: + optional 24-bit integer used to mutate the base DES algorithm. + defaults to ``0`` (no mutation applied). + + :arg rounds: + optional number of rounds of to apply the DES key schedule. + defaults to ``1``. + + :raises TypeError: if any of the provided args are of the wrong type. + :raises ValueError: + if any of the input blocks are the wrong size, + or the salt/rounds values are out of range. + + :returns: + resulting ciphertext as 64-bit integer. + """ + #--------------------------------------------------------------- + # input validation + #--------------------------------------------------------------- + + # validate salt, rounds + if rounds < 1: + raise ValueError("rounds must be positive integer") + if salt < 0 or salt > INT_24_MASK: + raise ValueError("salt must be 24-bit non-negative integer") + + # validate & unpack key + if not isinstance(key, int_types): + raise exc.ExpectedTypeError(key, "int", "key") + elif key < 0 or key > INT_64_MASK: + raise ValueError("key must be 64-bit non-negative integer") + + # validate & unpack input + if not isinstance(input, int_types): + raise exc.ExpectedTypeError(input, "int", "input") + elif input < 0 or input > INT_64_MASK: + raise ValueError("input must be 64-bit non-negative integer") + + #--------------------------------------------------------------- + # DES setup + #--------------------------------------------------------------- + # load tables if not already done + global SPE, PCXROT, IE3264, CF6464 + if PCXROT is None: + _load_tables() + + # load SPE into local vars to speed things up and remove an array access call + SPE0, SPE1, SPE2, SPE3, SPE4, SPE5, SPE6, SPE7 = SPE + + # NOTE: parity bits are ignored completely + # (UTs do fuzz testing to ensure this) + + # generate key schedule + # NOTE: generation was modified to output two elements at a time, + # so that per-round loop could do two passes at once. + def _iter_key_schedule(ks_odd): + """given 64-bit key, iterates over the 8 (even,odd) key schedule pairs""" + for p_even, p_odd in PCXROT: + ks_even = _permute(ks_odd, p_even) + ks_odd = _permute(ks_even, p_odd) + yield ks_even & _KS_MASK, ks_odd & _KS_MASK + ks_list = list(_iter_key_schedule(key)) + + # expand 24 bit salt -> 32 bit per des_crypt & bsdi_crypt + salt = ( + ((salt & 0x00003f) << 26) | + ((salt & 0x000fc0) << 12) | + ((salt & 0x03f000) >> 2) | + ((salt & 0xfc0000) >> 16) + ) + + # init L & R + if input == 0: + L = R = 0 + else: + L = ((input >> 31) & 0xaaaaaaaa) | (input & 0x55555555) + L = _permute(L, IE3264) + + R = ((input >> 32) & 0xaaaaaaaa) | ((input >> 1) & 0x55555555) + R = _permute(R, IE3264) + + #--------------------------------------------------------------- + # main DES loop - run for specified number of rounds + #--------------------------------------------------------------- + while rounds: + rounds -= 1 + + # run over each part of the schedule, 2 parts at a time + for ks_even, ks_odd in ks_list: + k = ((R>>32) ^ R) & salt # use the salt to flip specific bits + B = (k<<32) ^ k ^ R ^ ks_even + + L ^= (SPE0[(B>>58)&0x3f] ^ SPE1[(B>>50)&0x3f] ^ + SPE2[(B>>42)&0x3f] ^ SPE3[(B>>34)&0x3f] ^ + SPE4[(B>>26)&0x3f] ^ SPE5[(B>>18)&0x3f] ^ + SPE6[(B>>10)&0x3f] ^ SPE7[(B>>2)&0x3f]) + + k = ((L>>32) ^ L) & salt # use the salt to flip specific bits + B = (k<<32) ^ k ^ L ^ ks_odd + + R ^= (SPE0[(B>>58)&0x3f] ^ SPE1[(B>>50)&0x3f] ^ + SPE2[(B>>42)&0x3f] ^ SPE3[(B>>34)&0x3f] ^ + SPE4[(B>>26)&0x3f] ^ SPE5[(B>>18)&0x3f] ^ + SPE6[(B>>10)&0x3f] ^ SPE7[(B>>2)&0x3f]) + + # swap L and R + L, R = R, L + + #--------------------------------------------------------------- + # return final result + #--------------------------------------------------------------- + C = ( + ((L>>3) & 0x0f0f0f0f00000000) + | + ((L<<33) & 0xf0f0f0f000000000) + | + ((R>>35) & 0x000000000f0f0f0f) + | + ((R<<1) & 0x00000000f0f0f0f0) + ) + return _permute(C, CF6464) + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/crypto/digest.py b/ansible/lib/python3.11/site-packages/passlib/crypto/digest.py new file mode 100644 index 000000000..90e0cad56 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/crypto/digest.py @@ -0,0 +1,1057 @@ +"""passlib.crypto.digest -- crytographic helpers used by the password hashes in passlib + +.. versionadded:: 1.7 +""" +#============================================================================= +# imports +#============================================================================= +from __future__ import division +# core +import hashlib +import logging; log = logging.getLogger(__name__) +try: + # new in py3.4 + from hashlib import pbkdf2_hmac as _stdlib_pbkdf2_hmac + if _stdlib_pbkdf2_hmac.__module__ == "hashlib": + # builtin pure-python backends are slightly faster than stdlib's pure python fallback, + # so only using stdlib's version if it's backed by openssl's pbkdf2_hmac() + log.debug("ignoring pure-python hashlib.pbkdf2_hmac()") + _stdlib_pbkdf2_hmac = None +except ImportError: + _stdlib_pbkdf2_hmac = None +import re +import os +from struct import Struct +from warnings import warn +# site +try: + # https://pypi.python.org/pypi/fastpbkdf2/ + from fastpbkdf2 import pbkdf2_hmac as _fast_pbkdf2_hmac +except ImportError: + _fast_pbkdf2_hmac = None +# pkg +from passlib import exc +from passlib.utils import join_bytes, to_native_str, join_byte_values, to_bytes, \ + SequenceMixin, as_bool +from passlib.utils.compat import irange, int_types, unicode_or_bytes_types, PY3, error_from +from passlib.utils.decor import memoized_property +# local +__all__ = [ + # hash utils + "lookup_hash", + "HashInfo", + "norm_hash_name", + + # hmac utils + "compile_hmac", + + # kdfs + "pbkdf1", + "pbkdf2_hmac", +] + +#============================================================================= +# generic constants +#============================================================================= + +#: max 32-bit value +MAX_UINT32 = (1 << 32) - 1 + +#: max 64-bit value +MAX_UINT64 = (1 << 64) - 1 + +#============================================================================= +# hash utils +#============================================================================= + +#: list of known hash names, used by lookup_hash()'s _norm_hash_name() helper +_known_hash_names = [ + # format: (hashlib/ssl name, iana name or standin, other known aliases ...) + + #---------------------------------------------------- + # hashes with official IANA-assigned names + # (as of 2012-03 - http://www.iana.org/assignments/hash-function-text-names) + #---------------------------------------------------- + ("md2", "md2"), # NOTE: openssl dropped md2 support in v1.0.0 + ("md5", "md5"), + ("sha1", "sha-1"), + ("sha224", "sha-224", "sha2-224"), + ("sha256", "sha-256", "sha2-256"), + ("sha384", "sha-384", "sha2-384"), + ("sha512", "sha-512", "sha2-512"), + + # TODO: add sha3 to this table. + + #---------------------------------------------------- + # hashlib/ssl-supported hashes without official IANA names, + # (hopefully-) compatible stand-ins have been chosen. + #---------------------------------------------------- + + ("blake2b", "blake-2b"), + ("blake2s", "blake-2s"), + ("md4", "md4"), + # NOTE: there was an older "ripemd" and "ripemd-128", + # but python 2.7+ resolves "ripemd" -> "ripemd160", + # so treating "ripemd" as alias here. + ("ripemd160", "ripemd-160", "ripemd"), +] + + +#: dict mapping hashlib names to hardcoded digest info; +#: so this is available even when hashes aren't present. +_fallback_info = { + # name: (digest_size, block_size) + 'blake2b': (64, 128), + 'blake2s': (32, 64), + 'md4': (16, 64), + 'md5': (16, 64), + 'sha1': (20, 64), + 'sha224': (28, 64), + 'sha256': (32, 64), + 'sha384': (48, 128), + 'sha3_224': (28, 144), + 'sha3_256': (32, 136), + 'sha3_384': (48, 104), + 'sha3_512': (64, 72), + 'sha512': (64, 128), + 'shake128': (16, 168), + 'shake256': (32, 136), +} + + +def _gen_fallback_info(): + """ + internal helper used to generate ``_fallback_info`` dict. + currently only run manually to update the above list; + not invoked at runtime. + """ + out = {} + for alg in sorted(hashlib.algorithms_available | set(["md4"])): + info = lookup_hash(alg) + out[info.name] = (info.digest_size, info.block_size) + return out + + +#: cache of hash info instances used by lookup_hash() +_hash_info_cache = {} + +def _get_hash_aliases(name): + """ + internal helper used by :func:`lookup_hash` -- + normalize arbitrary hash name to hashlib format. + if name not recognized, returns dummy record and issues a warning. + + :arg name: + unnormalized name + + :returns: + tuple with 2+ elements: ``(hashlib_name, iana_name|None, ... 0+ aliases)``. + """ + + # normalize input + orig = name + if not isinstance(name, str): + name = to_native_str(name, 'utf-8', 'hash name') + name = re.sub("[_ /]", "-", name.strip().lower()) + if name.startswith("scram-"): # helper for SCRAM protocol (see passlib.handlers.scram) + name = name[6:] + if name.endswith("-plus"): + name = name[:-5] + + # look through standard names and known aliases + def check_table(name): + for row in _known_hash_names: + if name in row: + return row + result = check_table(name) + if result: + return result + + # try to clean name up some more + m = re.match(r"(?i)^(?P[a-z]+)-?(?P\d)?-?(?P\d{3,4})?$", name) + if m: + # roughly follows "SHA2-256" style format, normalize representation, + # and checked table. + iana_name, rev, size = m.group("name", "rev", "size") + if rev: + iana_name += rev + hashlib_name = iana_name + if size: + iana_name += "-" + size + if rev: + hashlib_name += "_" + hashlib_name += size + result = check_table(iana_name) + if result: + return result + + # not found in table, but roughly recognize format. use names we built up as fallback. + log.info("normalizing unrecognized hash name %r => %r / %r", + orig, hashlib_name, iana_name) + + else: + # just can't make sense of it. return something + iana_name = name + hashlib_name = name.replace("-", "_") + log.warning("normalizing unrecognized hash name and format %r => %r / %r", + orig, hashlib_name, iana_name) + + return hashlib_name, iana_name + + +def _get_hash_const(name): + """ + internal helper used by :func:`lookup_hash` -- + lookup hash constructor by name + + :arg name: + name (normalized to hashlib format, e.g. ``"sha256"``) + + :returns: + hash constructor, e.g. ``hashlib.sha256()``; + or None if hash can't be located. + """ + # check hashlib. for an efficient constructor + if not name.startswith("_") and name not in ("new", "algorithms"): + try: + return getattr(hashlib, name) + except AttributeError: + pass + + # check hashlib.new() in case SSL supports the digest + new_ssl_hash = hashlib.new + try: + # new() should throw ValueError if alg is unknown + new_ssl_hash(name, b"") + except ValueError: + pass + else: + # create wrapper function + # XXX: is there a faster way to wrap this? + def const(msg=b""): + return new_ssl_hash(name, msg) + const.__name__ = name + const.__module__ = "hashlib" + const.__doc__ = ("wrapper for hashlib.new(%r),\n" + "generated by passlib.crypto.digest.lookup_hash()") % name + return const + + # use builtin md4 as fallback when not supported by hashlib + if name == "md4": + from passlib.crypto._md4 import md4 + return md4 + + # XXX: any other modules / registries we should check? + # TODO: add pysha3 support. + + return None + + +def lookup_hash(digest, # *, + return_unknown=False, required=True): + """ + Returns a :class:`HashInfo` record containing information about a given hash function. + Can be used to look up a hash constructor by name, normalize hash name representation, etc. + + :arg digest: + This can be any of: + + * A string containing a :mod:`!hashlib` digest name (e.g. ``"sha256"``), + * A string containing an IANA-assigned hash name, + * A digest constructor function (e.g. ``hashlib.sha256``). + + Case is ignored, underscores are converted to hyphens, + and various other cleanups are made. + + :param required: + By default (True), this function will throw an :exc:`~passlib.exc.UnknownHashError` if no hash constructor + can be found, or if the hash is not actually available. + + If this flag is False, it will instead return a dummy :class:`!HashInfo` record + which will defer throwing the error until it's constructor function is called. + This is mainly used by :func:`norm_hash_name`. + + :param return_unknown: + + .. deprecated:: 1.7.3 + + deprecated, and will be removed in passlib 2.0. + this acts like inverse of **required**. + + :returns HashInfo: + :class:`HashInfo` instance containing information about specified digest. + + Multiple calls resolving to the same hash should always + return the same :class:`!HashInfo` instance. + """ + # check for cached entry + cache = _hash_info_cache + try: + return cache[digest] + except (KeyError, TypeError): + # NOTE: TypeError is to catch 'TypeError: unhashable type' (e.g. HashInfo) + pass + + # legacy alias + if return_unknown: + required = False + + # resolve ``digest`` to ``const`` & ``name_record`` + cache_by_name = True + if isinstance(digest, unicode_or_bytes_types): + # normalize name + name_list = _get_hash_aliases(digest) + name = name_list[0] + assert name + + # if name wasn't normalized to hashlib format, + # get info for normalized name and reuse it. + if name != digest: + info = lookup_hash(name, required=required) + cache[digest] = info + return info + + # else look up constructor + # NOTE: may return None, which is handled by HashInfo constructor + const = _get_hash_const(name) + + # if mock fips mode is enabled, replace with dummy constructor + # (to replicate how it would behave on a real fips system). + if const and mock_fips_mode and name not in _fips_algorithms: + def const(source=b""): + raise ValueError("%r disabled for fips by passlib set_mock_fips_mode()" % name) + + elif isinstance(digest, HashInfo): + # handle border case where HashInfo is passed in. + return digest + + elif callable(digest): + # try to lookup digest based on it's self-reported name + # (which we trust to be the canonical "hashlib" name) + const = digest + name_list = _get_hash_aliases(const().name) + name = name_list[0] + other_const = _get_hash_const(name) + if other_const is None: + # this is probably a third-party digest we don't know about, + # so just pass it on through, and register reverse lookup for it's name. + pass + + elif other_const is const: + # if we got back same constructor, this is just a known stdlib constructor, + # which was passed in before we had cached it by name. proceed normally. + pass + + else: + # if we got back different object, then ``const`` is something else + # (such as a mock object), in which case we want to skip caching it by name, + # as that would conflict with real hash. + cache_by_name = False + + else: + raise exc.ExpectedTypeError(digest, "digest name or constructor", "digest") + + # create new instance + info = HashInfo(const=const, names=name_list, required=required) + + # populate cache + if const is not None: + cache[const] = info + if cache_by_name: + for name in name_list: + if name: # (skips iana name if it's empty) + assert cache.get(name) in [None, info], "%r already in cache" % name + cache[name] = info + return info + +#: UT helper for clearing internal cache +lookup_hash.clear_cache = _hash_info_cache.clear + + +def norm_hash_name(name, format="hashlib"): + """Normalize hash function name (convenience wrapper for :func:`lookup_hash`). + + :arg name: + Original hash function name. + + This name can be a Python :mod:`~hashlib` digest name, + a SCRAM mechanism name, IANA assigned hash name, etc. + Case is ignored, and underscores are converted to hyphens. + + :param format: + Naming convention to normalize to. + Possible values are: + + * ``"hashlib"`` (the default) - normalizes name to be compatible + with Python's :mod:`!hashlib`. + + * ``"iana"`` - normalizes name to IANA-assigned hash function name. + For hashes which IANA hasn't assigned a name for, this issues a warning, + and then uses a heuristic to return a "best guess" name. + + :returns: + Hash name, returned as native :class:`!str`. + """ + info = lookup_hash(name, required=False) + if info.unknown: + warn("norm_hash_name(): " + info.error_text, exc.PasslibRuntimeWarning) + if format == "hashlib": + return info.name + elif format == "iana": + return info.iana_name + else: + raise ValueError("unknown format: %r" % (format,)) + + +class HashInfo(SequenceMixin): + """ + Record containing information about a given hash algorithm, as returned :func:`lookup_hash`. + + This class exposes the following attributes: + + .. autoattribute:: const + .. autoattribute:: digest_size + .. autoattribute:: block_size + .. autoattribute:: name + .. autoattribute:: iana_name + .. autoattribute:: aliases + .. autoattribute:: supported + + This object can also be treated a 3-element sequence + containing ``(const, digest_size, block_size)``. + """ + #========================================================================= + # instance attrs + #========================================================================= + + #: Canonical / hashlib-compatible name (e.g. ``"sha256"``). + name = None + + #: IANA assigned name (e.g. ``"sha-256"``), may be ``None`` if unknown. + iana_name = None + + #: Tuple of other known aliases (may be empty) + aliases = () + + #: Hash constructor function (e.g. :func:`hashlib.sha256`) + const = None + + #: Hash's digest size + digest_size = None + + #: Hash's block size + block_size = None + + #: set when hash isn't available, will be filled in with string containing error text + #: that const() will raise. + error_text = None + + #: set when error_text is due to hash algorithm being completely unknown + #: (not just unavailable on current system) + unknown = False + + #========================================================================= + # init + #========================================================================= + + def __init__(self, # *, + const, names, required=True): + """ + initialize new instance. + :arg const: + hash constructor + :arg names: + list of 2+ names. should be list of ``(name, iana_name, ... 0+ aliases)``. + names must be lower-case. only iana name may be None. + """ + # init names + name = self.name = names[0] + self.iana_name = names[1] + self.aliases = names[2:] + + def use_stub_const(msg): + """ + helper that installs stub constructor which throws specified error . + """ + def const(source=b""): + raise exc.UnknownHashError(msg, name) + if required: + # if caller only wants supported digests returned, + # just throw error immediately... + const() + assert "shouldn't get here" + self.error_text = msg + self.const = const + try: + self.digest_size, self.block_size = _fallback_info[name] + except KeyError: + pass + + # handle "constructor not available" case + if const is None: + if names in _known_hash_names: + msg = "unsupported hash: %r" % name + else: + msg = "unknown hash: %r" % name + self.unknown = True + use_stub_const(msg) + # TODO: load in preset digest size info for known hashes. + return + + # create hash instance to inspect + try: + hash = const() + except ValueError as err: + # per issue 116, FIPS compliant systems will have a constructor; + # but it will throw a ValueError with this message. As of 1.7.3, + # translating this into DisabledHashError. + # "ValueError: error:060800A3:digital envelope routines:EVP_DigestInit_ex:disabled for fips" + if "disabled for fips" in str(err).lower(): + msg = "%r hash disabled for fips" % name + else: + msg = "internal error in %r constructor\n(%s: %s)" % (name, type(err).__name__, err) + use_stub_const(msg) + return + + # store stats about hash + self.const = const + self.digest_size = hash.digest_size + self.block_size = hash.block_size + + # do sanity check on digest size + if len(hash.digest()) != hash.digest_size: + raise RuntimeError("%r constructor failed sanity check" % self.name) + + # do sanity check on name. + if hash.name != self.name: + warn("inconsistent digest name: %r resolved to %r, which reports name as %r" % + (self.name, const, hash.name), exc.PasslibRuntimeWarning) + + #========================================================================= + # methods + #========================================================================= + def __repr__(self): + return " digest output``. + + However, if ``multipart=True``, the returned function has the signature + ``hmac() -> update, finalize``, where ``update(msg)`` may be called multiple times, + and ``finalize() -> digest_output`` may be repeatedly called at any point to + calculate the HMAC digest so far. + + The returned object will also have a ``digest_info`` attribute, containing + a :class:`lookup_hash` instance for the specified digest. + + This function exists, and has the weird signature it does, in order to squeeze as + provide as much efficiency as possible, by omitting much of the setup cost + and features of the stdlib :mod:`hmac` module. + """ + # all the following was adapted from stdlib's hmac module + + # resolve digest (cached) + digest_info = lookup_hash(digest) + const, digest_size, block_size = digest_info + assert block_size >= 16, "block size too small" + + # prepare key + if not isinstance(key, bytes): + key = to_bytes(key, param="key") + klen = len(key) + if klen > block_size: + key = const(key).digest() + klen = digest_size + if klen < block_size: + key += b'\x00' * (block_size - klen) + + # create pre-initialized hash constructors + _inner_copy = const(key.translate(_TRANS_36)).copy + _outer_copy = const(key.translate(_TRANS_5C)).copy + + if multipart: + # create multi-part function + # NOTE: this is slightly slower than the single-shot version, + # and should only be used if needed. + def hmac(): + """generated by compile_hmac(multipart=True)""" + inner = _inner_copy() + def finalize(): + outer = _outer_copy() + outer.update(inner.digest()) + return outer.digest() + return inner.update, finalize + else: + + # single-shot function + def hmac(msg): + """generated by compile_hmac()""" + inner = _inner_copy() + inner.update(msg) + outer = _outer_copy() + outer.update(inner.digest()) + return outer.digest() + + # add info attr + hmac.digest_info = digest_info + return hmac + +#============================================================================= +# pbkdf1 +#============================================================================= +def pbkdf1(digest, secret, salt, rounds, keylen=None): + """pkcs#5 password-based key derivation v1.5 + + :arg digest: + digest name or constructor. + + :arg secret: + secret to use when generating the key. + may be :class:`!bytes` or :class:`unicode` (encoded using UTF-8). + + :arg salt: + salt string to use when generating key. + may be :class:`!bytes` or :class:`unicode` (encoded using UTF-8). + + :param rounds: + number of rounds to use to generate key. + + :arg keylen: + number of bytes to generate (if omitted / ``None``, uses digest's native size) + + :returns: + raw :class:`bytes` of generated key + + .. note:: + + This algorithm has been deprecated, new code should use PBKDF2. + Among other limitations, ``keylen`` cannot be larger + than the digest size of the specified hash. + """ + # resolve digest + const, digest_size, block_size = lookup_hash(digest) + + # validate secret & salt + secret = to_bytes(secret, param="secret") + salt = to_bytes(salt, param="salt") + + # validate rounds + if not isinstance(rounds, int_types): + raise exc.ExpectedTypeError(rounds, "int", "rounds") + if rounds < 1: + raise ValueError("rounds must be at least 1") + + # validate keylen + if keylen is None: + keylen = digest_size + elif not isinstance(keylen, int_types): + raise exc.ExpectedTypeError(keylen, "int or None", "keylen") + elif keylen < 0: + raise ValueError("keylen must be at least 0") + elif keylen > digest_size: + raise ValueError("keylength too large for digest: %r > %r" % + (keylen, digest_size)) + + # main pbkdf1 loop + block = secret + salt + for _ in irange(rounds): + block = const(block).digest() + return block[:keylen] + +#============================================================================= +# pbkdf2 +#============================================================================= + +_pack_uint32 = Struct(">L").pack + +def pbkdf2_hmac(digest, secret, salt, rounds, keylen=None): + """pkcs#5 password-based key derivation v2.0 using HMAC + arbitrary digest. + + :arg digest: + digest name or constructor. + + :arg secret: + passphrase to use to generate key. + may be :class:`!bytes` or :class:`unicode` (encoded using UTF-8). + + :arg salt: + salt string to use when generating key. + may be :class:`!bytes` or :class:`unicode` (encoded using UTF-8). + + :param rounds: + number of rounds to use to generate key. + + :arg keylen: + number of bytes to generate. + if omitted / ``None``, will use digest's native output size. + + :returns: + raw bytes of generated key + + .. versionchanged:: 1.7 + + This function will use the first available of the following backends: + + * `fastpbk2 `_ + * :func:`hashlib.pbkdf2_hmac` (only available in py2 >= 2.7.8, and py3 >= 3.4) + * builtin pure-python backend + + See :data:`passlib.crypto.digest.PBKDF2_BACKENDS` to determine + which backend(s) are in use. + """ + # validate secret & salt + secret = to_bytes(secret, param="secret") + salt = to_bytes(salt, param="salt") + + # resolve digest + digest_info = lookup_hash(digest) + digest_size = digest_info.digest_size + + # validate rounds + if not isinstance(rounds, int_types): + raise exc.ExpectedTypeError(rounds, "int", "rounds") + if rounds < 1: + raise ValueError("rounds must be at least 1") + + # validate keylen + if keylen is None: + keylen = digest_size + elif not isinstance(keylen, int_types): + raise exc.ExpectedTypeError(keylen, "int or None", "keylen") + elif keylen < 1: + # XXX: could allow keylen=0, but want to be compat w/ stdlib + raise ValueError("keylen must be at least 1") + + # find smallest block count s.t. keylen <= block_count * digest_size; + # make sure block count won't overflow (per pbkdf2 spec) + # this corresponds to throwing error if keylen > digest_size * MAX_UINT32 + # NOTE: stdlib will throw error at lower bound (keylen > MAX_SINT32) + # NOTE: have do this before other backends checked, since fastpbkdf2 raises wrong error + # (InvocationError, not OverflowError) + block_count = (keylen + digest_size - 1) // digest_size + if block_count > MAX_UINT32: + raise OverflowError("keylen too long for digest") + + # + # check for various high-speed backends + # + + # ~3x faster than pure-python backend + # NOTE: have to do this after above guards since fastpbkdf2 lacks bounds checks. + if digest_info.supported_by_fastpbkdf2: + return _fast_pbkdf2_hmac(digest_info.name, secret, salt, rounds, keylen) + + # ~1.4x faster than pure-python backend + # NOTE: have to do this after fastpbkdf2 since hashlib-ssl is slower, + # will support larger number of hashes. + if digest_info.supported_by_hashlib_pbkdf2: + return _stdlib_pbkdf2_hmac(digest_info.name, secret, salt, rounds, keylen) + + # + # otherwise use our own implementation + # + + # generated keyed hmac + keyed_hmac = compile_hmac(digest, secret) + + # get helper to calculate pbkdf2 inner loop efficiently + calc_block = _get_pbkdf2_looper(digest_size) + + # assemble & return result + return join_bytes( + calc_block(keyed_hmac, keyed_hmac(salt + _pack_uint32(i)), rounds) + for i in irange(1, block_count + 1) + )[:keylen] + +#------------------------------------------------------------------------------------- +# pick best choice for pure-python helper +# TODO: consider some alternatives, such as C-accelerated xor_bytes helper if available +#------------------------------------------------------------------------------------- +# NOTE: this env var is only present to support the admin/benchmark_pbkdf2 script +_force_backend = os.environ.get("PASSLIB_PBKDF2_BACKEND") or "any" + +if PY3 and _force_backend in ["any", "from-bytes"]: + from functools import partial + + def _get_pbkdf2_looper(digest_size): + return partial(_pbkdf2_looper, digest_size) + + def _pbkdf2_looper(digest_size, keyed_hmac, digest, rounds): + """ + py3-only implementation of pbkdf2 inner loop; + uses 'int.from_bytes' + integer XOR + """ + from_bytes = int.from_bytes + BIG = "big" # endianess doesn't matter, just has to be consistent + accum = from_bytes(digest, BIG) + for _ in irange(rounds - 1): + digest = keyed_hmac(digest) + accum ^= from_bytes(digest, BIG) + return accum.to_bytes(digest_size, BIG) + + _builtin_backend = "from-bytes" + +elif _force_backend in ["any", "unpack", "from-bytes"]: + from struct import Struct + from passlib.utils import sys_bits + + _have_64_bit = (sys_bits >= 64) + + #: cache used by _get_pbkdf2_looper + _looper_cache = {} + + def _get_pbkdf2_looper(digest_size): + """ + We want a helper function which performs equivalent of the following:: + + def helper(keyed_hmac, digest, rounds): + accum = digest + for _ in irange(rounds - 1): + digest = keyed_hmac(digest) + accum ^= digest + return accum + + However, no efficient way to implement "bytes ^ bytes" in python. + Instead, using approach where we dynamically compile a helper function based + on digest size. Instead of a single `accum` var, this helper breaks the digest + into a series of integers. + + It stores these in a series of`accum_` vars, and performs `accum ^= digest` + by unpacking digest and perform xor for each "accum_ ^= digest_". + this keeps everything in locals, avoiding excessive list creation, encoding or decoding, + etc. + + :param digest_size: + digest size to compile for, in bytes. (must be multiple of 4). + + :return: + helper function with call signature outlined above. + """ + # + # cache helpers + # + try: + return _looper_cache[digest_size] + except KeyError: + pass + + # + # figure out most efficient struct format to unpack digest into list of native ints + # + if _have_64_bit and not digest_size & 0x7: + # digest size multiple of 8, on a 64 bit system -- use array of UINT64 + count = (digest_size >> 3) + fmt = "=%dQ" % count + elif not digest_size & 0x3: + if _have_64_bit: + # digest size multiple of 4, on a 64 bit system -- use array of UINT64 + 1 UINT32 + count = (digest_size >> 3) + fmt = "=%dQI" % count + count += 1 + else: + # digest size multiple of 4, on a 32 bit system -- use array of UINT32 + count = (digest_size >> 2) + fmt = "=%dI" % count + else: + # stopping here, cause no known hashes have digest size that isn't multiple of 4 bytes. + # if needed, could go crazy w/ "H" & "B" + raise NotImplementedError("unsupported digest size: %d" % digest_size) + struct = Struct(fmt) + + # + # build helper source + # + tdict = dict( + digest_size=digest_size, + accum_vars=", ".join("acc_%d" % i for i in irange(count)), + digest_vars=", ".join("dig_%d" % i for i in irange(count)), + ) + + # head of function + source = ( + "def helper(keyed_hmac, digest, rounds):\n" + " '''pbkdf2 loop helper for digest_size={digest_size}'''\n" + " unpack_digest = struct.unpack\n" + " {accum_vars} = unpack_digest(digest)\n" + " for _ in irange(1, rounds):\n" + " digest = keyed_hmac(digest)\n" + " {digest_vars} = unpack_digest(digest)\n" + ).format(**tdict) + + # xor digest + for i in irange(count): + source += " acc_%d ^= dig_%d\n" % (i, i) + + # return result + source += " return struct.pack({accum_vars})\n".format(**tdict) + + # + # compile helper + # + code = compile(source, "", "exec") + gdict = dict(irange=irange, struct=struct) + ldict = dict() + eval(code, gdict, ldict) + helper = ldict['helper'] + if __debug__: + helper.__source__ = source + + # + # store in cache + # + _looper_cache[digest_size] = helper + return helper + + _builtin_backend = "unpack" + +else: + assert _force_backend in ["any", "hexlify"] + + # XXX: older & slower approach that used int(hexlify()), + # keeping it around for a little while just for benchmarking. + + from binascii import hexlify as _hexlify + from passlib.utils import int_to_bytes + + def _get_pbkdf2_looper(digest_size): + return _pbkdf2_looper + + def _pbkdf2_looper(keyed_hmac, digest, rounds): + hexlify = _hexlify + accum = int(hexlify(digest), 16) + for _ in irange(rounds - 1): + digest = keyed_hmac(digest) + accum ^= int(hexlify(digest), 16) + return int_to_bytes(accum, len(digest)) + + _builtin_backend = "hexlify" + +# helper for benchmark script -- disable hashlib, fastpbkdf2 support if builtin requested +if _force_backend == _builtin_backend: + _fast_pbkdf2_hmac = _stdlib_pbkdf2_hmac = None + +# expose info about what backends are active +PBKDF2_BACKENDS = [b for b in [ + "fastpbkdf2" if _fast_pbkdf2_hmac else None, + "hashlib-ssl" if _stdlib_pbkdf2_hmac else None, + "builtin-" + _builtin_backend +] if b] + +# *very* rough estimate of relative speed (compared to sha256 using 'unpack' backend on 64bit arch) +if "fastpbkdf2" in PBKDF2_BACKENDS: + PBKDF2_SPEED_FACTOR = 3 +elif "hashlib-ssl" in PBKDF2_BACKENDS: + PBKDF2_SPEED_FACTOR = 1.4 +else: + # remaining backends have *some* difference in performance, but not enough to matter + PBKDF2_SPEED_FACTOR = 1 + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/crypto/scrypt/__init__.py b/ansible/lib/python3.11/site-packages/passlib/crypto/scrypt/__init__.py new file mode 100644 index 000000000..c71873abb --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/crypto/scrypt/__init__.py @@ -0,0 +1,281 @@ +""" +passlib.utils.scrypt -- scrypt hash frontend and help utilities + +XXX: add this module to public docs? +""" +#========================================================================== +# imports +#========================================================================== +from __future__ import absolute_import +# core +import logging; log = logging.getLogger(__name__) +from warnings import warn +# pkg +from passlib import exc +from passlib.utils import to_bytes +from passlib.utils.compat import PYPY +# local +__all__ =[ + "validate", + "scrypt", +] + +#========================================================================== +# config validation +#========================================================================== + +#: internal global constant for setting stdlib scrypt's maxmem (int bytes). +#: set to -1 to auto-calculate (see _load_stdlib_backend() below) +#: set to 0 for openssl default (32mb according to python docs) +#: TODO: standardize this across backends, and expose support via scrypt hash config; +#: currently not very configurable, and only applies to stdlib backend. +SCRYPT_MAXMEM = -1 + +#: max output length in bytes +MAX_KEYLEN = ((1 << 32) - 1) * 32 + +#: max ``r * p`` limit +MAX_RP = (1 << 30) - 1 + +# TODO: unittests for this function +def validate(n, r, p): + """ + helper which validates a set of scrypt config parameters. + scrypt will take ``O(n * r * p)`` time and ``O(n * r)`` memory. + limitations are that ``n = 2**``, ``n < 2**(16*r)``, ``r * p < 2 ** 30``. + + :param n: scrypt rounds + :param r: scrypt block size + :param p: scrypt parallel factor + """ + if r < 1: + raise ValueError("r must be > 0: r=%r" % r) + + if p < 1: + raise ValueError("p must be > 0: p=%r" % p) + + if r * p > MAX_RP: + # pbkdf2-hmac-sha256 limitation - it will be requested to generate ``p*(2*r)*64`` bytes, + # but pbkdf2 can do max of (2**31-1) blocks, and sha-256 has 32 byte block size... + # so ``(2**31-1)*32 >= p*r*128`` -> ``r*p < 2**30`` + raise ValueError("r * p must be < 2**30: r=%r, p=%r" % (r,p)) + + if n < 2 or n & (n - 1): + raise ValueError("n must be > 1, and a power of 2: n=%r" % n) + + return True + + +UINT32_SIZE = 4 + + +def estimate_maxmem(n, r, p, fudge=1.05): + """ + calculate memory required for parameter combination. + assumes parameters have already been validated. + + .. warning:: + this is derived from OpenSSL's scrypt maxmem formula; + and may not be correct for other implementations + (additional buffers, different parallelism tradeoffs, etc). + """ + # XXX: expand to provide upper bound for diff backends, or max across all of them? + # NOTE: openssl's scrypt() enforces it's maxmem parameter based on calc located at + # , ending in line containing "Blen + Vlen > maxmem" + # using the following formula: + # Blen = p * 128 * r + # Vlen = 32 * r * (N + 2) * sizeof(uint32_t) + # total_bytes = Blen + Vlen + maxmem = r * (128 * p + 32 * (n + 2) * UINT32_SIZE) + # add fudge factor so we don't have off-by-one mismatch w/ openssl + maxmem = int(maxmem * fudge) + return maxmem + + +# TODO: configuration picker (may need psutil for full effect) + +#========================================================================== +# hash frontend +#========================================================================== + +#: backend function used by scrypt(), filled in by _set_backend() +_scrypt = None + +#: name of backend currently in use, exposed for informational purposes. +backend = None + +def scrypt(secret, salt, n, r, p=1, keylen=32): + """run SCrypt key derivation function using specified parameters. + + :arg secret: + passphrase string (unicode is encoded to bytes using utf-8). + + :arg salt: + salt string (unicode is encoded to bytes using utf-8). + + :arg n: + integer 'N' parameter + + :arg r: + integer 'r' parameter + + :arg p: + integer 'p' parameter + + :arg keylen: + number of bytes of key to generate. + defaults to 32 (the internal block size). + + :returns: + a *keylen*-sized bytes instance + + SCrypt imposes a number of constraints on it's input parameters: + + * ``r * p < 2**30`` -- due to a limitation of PBKDF2-HMAC-SHA256. + * ``keylen < (2**32 - 1) * 32`` -- due to a limitation of PBKDF2-HMAC-SHA256. + * ``n`` must a be a power of 2, and > 1 -- internal limitation of scrypt() implementation + + :raises ValueError: if the provided parameters are invalid (see constraints above). + + .. warning:: + + Unless the third-party ``scrypt ``_ package + is installed, passlib will use a builtin pure-python implementation of scrypt, + which is *considerably* slower (and thus requires a much lower / less secure + ``n`` value in order to be usuable). Installing the :mod:`!scrypt` package + is strongly recommended. + """ + validate(n, r, p) + secret = to_bytes(secret, param="secret") + salt = to_bytes(salt, param="salt") + if keylen < 1: + raise ValueError("keylen must be at least 1") + if keylen > MAX_KEYLEN: + raise ValueError("keylen too large, must be <= %d" % MAX_KEYLEN) + return _scrypt(secret, salt, n, r, p, keylen) + + +def _load_builtin_backend(): + """ + Load pure-python scrypt implementation built into passlib. + """ + slowdown = 10 if PYPY else 100 + warn("Using builtin scrypt backend, which is %dx slower than is required " + "for adequate security. Installing scrypt support (via 'pip install scrypt') " + "is strongly recommended" % slowdown, exc.PasslibSecurityWarning) + from ._builtin import ScryptEngine + return ScryptEngine.execute + + +def _load_cffi_backend(): + """ + Try to import the ctypes-based scrypt hash function provided by the + ``scrypt ``_ package. + """ + try: + from scrypt import hash + return hash + except ImportError: + pass + # not available, but check to see if package present but outdated / not installed right + try: + import scrypt + except ImportError as err: + if "scrypt" not in str(err): + # e.g. if cffi isn't set up right + # user should try importing scrypt explicitly to diagnose problem. + warn("'scrypt' package failed to import correctly (possible installation issue?)", + exc.PasslibWarning) + # else: package just isn't installed + else: + warn("'scrypt' package is too old (lacks ``hash()`` method)", exc.PasslibWarning) + return None + + +def _load_stdlib_backend(): + """ + Attempt to load stdlib scrypt() implement and return wrapper. + Returns None if not found. + """ + try: + # new in python 3.6, if compiled with openssl >= 1.1 + from hashlib import scrypt as stdlib_scrypt + except ImportError: + return None + + def stdlib_scrypt_wrapper(secret, salt, n, r, p, keylen): + # work out appropriate "maxmem" parameter + # + # TODO: would like to enforce a single "maxmem" policy across all backends; + # and maybe expose this via scrypt hasher config. + # + # for now, since parameters should all be coming from internally-controlled sources + # (password hashes), using policy of "whatever memory the parameters needs". + # furthermore, since stdlib scrypt is only place that needs this, + # currently calculating exactly what maxmem needs to make things work for stdlib call. + # as hack, this can be overriden via SCRYPT_MAXMEM above, + # would like to formalize all of this. + maxmem = SCRYPT_MAXMEM + if maxmem < 0: + maxmem = estimate_maxmem(n, r, p) + return stdlib_scrypt(password=secret, salt=salt, n=n, r=r, p=p, dklen=keylen, + maxmem=maxmem) + + return stdlib_scrypt_wrapper + + +#: list of potential backends +backend_values = ("stdlib", "scrypt", "builtin") + +#: dict mapping backend name -> loader +_backend_loaders = dict( + stdlib=_load_stdlib_backend, + scrypt=_load_cffi_backend, # XXX: rename backend constant to "cffi"? + builtin=_load_builtin_backend, +) + + +def _set_backend(name, dryrun=False): + """ + set backend for scrypt(). if name not specified, loads first available. + + :raises ~passlib.exc.MissingBackendError: if backend can't be found + + .. note:: mainly intended to be called by unittests, and scrypt hash handler + """ + if name == "any": + return + elif name == "default": + for name in backend_values: + try: + return _set_backend(name, dryrun=dryrun) + except exc.MissingBackendError: + continue + raise exc.MissingBackendError("no scrypt backends available") + else: + loader = _backend_loaders.get(name) + if not loader: + raise ValueError("unknown scrypt backend: %r" % (name,)) + hash = loader() + if not hash: + raise exc.MissingBackendError("scrypt backend %r not available" % name) + if dryrun: + return + global _scrypt, backend + backend = name + _scrypt = hash + +# initialize backend +_set_backend("default") + + +def _has_backend(name): + try: + _set_backend(name, dryrun=True) + return True + except exc.MissingBackendError: + return False + +#========================================================================== +# eof +#========================================================================== diff --git a/ansible/lib/python3.11/site-packages/passlib/crypto/scrypt/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/crypto/scrypt/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3ddaeba37ee94203a6f469e8af09d234a80f2e09 GIT binary patch literal 8671 zcmbtZTWlLwdY&PN7m*@$mn_GQk1vWuMVCmnQ&(}Ebs}l)#&(>@*|miR)QB^b#vaZv zXNI;#P>JddP;RgRl!t5_q#LL&*=&;ndFVq>V1WchfqiHTG9XZbfB*wUP#9Pg43ZQ^ zP;~p9Gcyz^D`>GD4u77RbN|nO`Tn23j>RGZt`AoJZsmg^LHIZJ;9UW?@XhxqyelZe zqM(S1PZu-3MW4vZe%-(5_X(>1y5x0I10RZl@G-v5wHQP@Ne^Yhi(%d;ptofrixFN9 z>d{R5VmmK~^jIdo7)Lp*w5|9SJCw*`rxIQ4QrZ{0mDn4?VviDE>{SjZoo|SXeTrZ1 z$9F(EsPy1@pVEsKgGwLhex)Du5oKWU0M>R;eMH@_4t_H5p&#qQ*In16%A?94dLBZX zL(2ZvRbv&8U<{)87lg#2{7NJ%Tb8aZPvmS(wGCt5>hi5Lr=(y{cIxV=6gaCALYjxn*5T5yebd&qVICwI#$_hrDc= zdd^mpS|)2!`x4$sw`6MI*RS4A;U{dH$>lX$wOG~VYnQLp!Z&4IQ)FAM1)Wvh{{t?a z?fBm7e|Y_MLBr!SsRjq|K8P=S{^s`}?+OJ`5pSR*2(;U7oS+~mzJi!X+g2Xk5evQ_ zXzwrh_H2(Hc}SJ*`a({*CYH&n8;^umZiMhV3?n^;;Rww6)3NHl8L+l-nq>6$Z!u4Ob^wl&kh zv=m~oD%%*xAWxI&(b3ac(}ED)REY-hT2bklrKK@meVSE|Og%YDF(P}#=OlPdMn}o< zCzh5bB7BiEeAUF5@is%voS|6Fb+l2ptedGD#M1I=bA7f^&&JYqRVQgVWt)^Yy3eE~ z-$O|zXW3*~C1=PJGlV`pMDsn_)|XlKa?n|+*P3tSIPZ~{#z^+*`%FUeLq=2AsWHw( znPkmdI4&H_^b9dD*p-B@7Ws*+=hQh$O~`{ng1; zGowyg897JI$t*RMN!hTpWnG{Zh^J~1^l3687dSvhq>UQw+{SNkWq+MAQk znNKE@nxWarl?Eq1?J5sWm*dAP@#CfN z@q3~0|5EuExwmfKd2Qph@4`aIVDb9rBL5uz{BUV-wj7_U#OF%kxyCS?;^vEYyWW2J z?WjB7k3OF&H#dxS6i+N0zso-a|ts1;86(ne&BtPS6U`p0gIUSbE)e1t@S92Ow6_PgT zj>eIcnOW8ht|*S;z$oW3s613wqA zn^x<3tbvw3!R8iwc^$jW8()CWD!>!6eH8*9V&sn-qeCx3@(C=4<_xm%97h>9)HO#e zx!{v@&PXwdAvp^oOsuS$($X44w%Idr0h^I2>eUog?Hxg9rYyTkWlJTNP1y(|IYUdC z3fzlDRD+cOQcS|Zg*!>kPLH30$TbZs>rI1Nc=~@0XY4G{b#i3*((ulfnr5bZG^Kkt z&F;}OyLVGCSy!7@VdOH)j+b%x0C&UA0eo3e4V6MY90g>0qo`>)r`r~*Jw8oFpz+*Q zQ?AtwZ`$A=&Lc++YiDsX8Fl6#9cOM=ae1#9mMt48)!Do&4Ga)kFnKcBSt6hb&Qb>F zECRvMY*;MK$mSda;-HloXOz)qlrTEVFam?IN{)k3ne0Fj=3y^C`~2+u^!T}pXP+Bi zICpmX>*(lD60X9YeLa^;|GL-=2IX?YIk7zJh#PEa65Ja=1cU=D&GR4-)1cbA; zb*+w59!c!7wXWoVJB({PFkyyhX;^)g00rFCn9T3!4+mkI!LcA2u~ZeFXNSY`vUyYW z4ztCXG|c+ThOSzW2MiC#LKPqhYOjH5E?PXjYTH?BW)g@as{uYR*fC+!75?nO3&6Z2 z7R4b*V`S(k7Yld=1whG(N(e`vIjA%^m(z4xGZ0r$02n^R=$!9@rcR%B8P%Porv zvy#IE;37HCgk0B|A!6*BL1Z+ublBn3(q7w#q?^WyzJ{d$55TA5>>Q~?+{29>BBB64 z%$mdu6Zbj!L04Wq$O(k4t1^UbDu2kWvQ3ldu$k&u19qN%nhYuMpd8*YD^24dwFrZl z&(B@EFn6gIN;->81Sw$+jxe&z;|84>EH(nO7_P6S%qMWd{E+n~$cFIUOQH~n6<-9L zn<`6v6{)Wz^=-%cHq%?@O0n^BY`hX1-1xwN4a&)*79p0F&hGLuIo%}|g zgUu7=_{mEAWGQ?S@85}h8QNb8?dLT!zg;X3{+II6 zA3au%PF12)C25NL>lBvMxEN0KC0bR-j)uwdEnv(&1e?n)}+#^ zDIgZ{QwQJH!bx|7l!Ya=ka`;pY!xsu%rsjuZyCJCV!+7*fPG0FZ1#F4U7pYhH2DBu z>!%w@ez(J~aNxbz~-NhPK_S;A@R4sD=`y;?VJY@Hj$brQgLP7PJ1ayNN)hDpHTJ${6 zp7FFmleWZNZSl0k56w&XDeJ;JBgICkmospJqHaegQt7nTP>7Rg|2OzrUw|Mkib#Q2 zHGHVldRIGoiqQw#(f+z124Xv;8g4I2bwA!cc<`Vej06_NYPhc$W1sK3g|=8JHv5mM z%KX)G_*x}=t;FvK-*pM?19c$~7}_D#XwR>X-?iR5^;f4#(WAJ(3ktEWQr9cnUA>jA zqhI^Q*wD8E2qupWVK(Qgw_y|%>)k?oXYqt}7`wO=iU@B;2K>Ji2mJ3#Gs53T&i<+Y z3+eH*C;VTW2%x;TKbS?=zZ9Fb^G(=rqw68vk(h2WY4#+$D5jAeej~ijbvY5pr@4%u zwQV(n1S`&;*&>)m1^F3QwwoayPYF4An%tst7KuDZI$v@yHo0UP03T@{nWtIOt3`PE zabIc)MVa)lIQSa{AS$990U{!pvLU0#4c!L)=tl$**~zR zOCVSw)8O8FVV0ZQb$j~T%k2(xwED$pO}iIe|eb^i$h8_+4hG3V~A zZ>(3tahR*N*yiAuZTm}Y`?q_K)CH++kdxwU-RF-DZVw#Yn*FrDG;pdsaH=wJs?_P; z54L;yD?Ovk<)VW}s-62b!!Q}KL70rM;$4;aQ0cLqRQ^|8hyS%}=*zC5Ev4M`c%|!c z%-_++jNY8M-F;wl^s_6UT`3)WuG~Fa>7FgM&!VEsLBZYaMSc&=a}!4jx&MZif01!Iq)I{ zgVVSe<2Vfe2@1^7x1?zINWdbT6S-k&GG^BsWf+!+pg=;pbcN1h_<3M4el$!L=sA#t zNSPC{*oxdNb$b%h>`4TD0TrF7wOD@~qE8C+ZHId|uWTJFg@?=G;YxTI1#Yup%j1fr5FW2;Pel zR0O0`5Qj^`W6k$HA-=&srT*c~l}i8cN8xh+RHc7v(_a+^ORe`kVW8A{SA`>`*1IYU zm0E8WV0Kic_Ikh{@oj|a2ZXMkjTqj>k8DKn6B*bD)`Np$XI*HJQ$k1QMzkL66}#&~ zD>*GhJ1|Pn`H`Lwr|Uv1NsDbFsS6FVub++GAkzoM!*!uSo)v}esf{=W?A^C{q8dF^ zjUKGFAFhX_UfLy-5`$yAGh<48 zwi>NiF^Qs+S+OG3Rd=a$;)1$8QE6AAQddGJowR?Q?8?$wBSk{GTdC+yI``O0RsZSz zo_G8;hJNVw?#A}xnfLQ~pZ9rx&&T{jYil!y>u+a&J^L>^IqqNSp>jFPH&3tN<}N33 zQ=G(0wkSWvvwz!^4gdD2ecmzU;Hl0Lb5MV?nj*DzKJKf z`JSEQKEThsrdlN11up1G9Z86)8jVa3CACOY9a6)}LP8q~O(!E!EfNz32L-dJKPX(2 zW&{)^l8QW-SkUI;u_rXxM36TekyIps|CYowT8)P0=EX1`-KrQ>#nJtS_W~RDwb*PV zCWp&Bus>V*;ptV}+~s5r>*Q#SQ+CNV<-pI0p9?=Xejfaq@N1UrSb=@YE4NCHo7_G0 z{h&M$$4#|K&YRqnPjZ3!Y5h~}k_WUyY69()>{xlT;z9>;6VGwY0b#g#;=>6gE)9z@H8LHQhj9RgO_e?{G%_-*Ml`56#3o`thRebl zW-1&XHpMhtIU+-e1;Z`hki$t$PIcAm`B1rZ6g;T?Ad6hd#rZpRH9L|OSKF7;E9u|+ zUij4a!kTN{u|AXc9V++^F*h?5Id>N$v(3!%=u zuqj-qa#p2`pN0`qsFEB+ts{fOmP#-1=Q8I} zyjY>PTF!-9tM|O*zQ*`cy0z_GO7|g!s4RRj(?+>0BcrOA~OrApyhCcLXlWR3x$S`MB_N#>an3( z(@W^8?gaUAkt=TM$*QX(x3Ay1zSfD)`S$~VA6V~rzbC(GU(UZzp_K&N7202=jR@_d z(X7r#ZiJ>6G+8w~k?WPaX(|OS7_IDXPK?g5N0+K8$*^X42x!zuN;X^<%o00lj270q z+{PVZPa}qd{tZtYc0<(S3f8L0^Ru$1$%;bE2R%R~IXct8o3fj@qy~yWq!&b`gTqit zc;yvSa-NESQr*v$+hOWP=Icuc>2Z69?pSdZt$#gRSKoSf4=Q}c&D-Adt@q|P@5NJV zv2WKp|NdCc6QrkAPjUN>wcd9pbw|O|v*GJrd1qD4`}zvLzO}bL_4Vf}-;J%k?;Tz5 z{7`-H(*4Y@GWp<8TzkLhDHqgOw9~1w={4q?}K?ATMvu$xdN7r26#&8saxb~8CkRmT1GKIJ?XpL5tX0P~M>i<}g6^5I!WaH-Pv##P}=yHo$jOh0L&)%Rh}gPM@Tk(mg*57X$v zA%fA}Cx~z^OnY1NSl|o6Uy$S z^8B++>zl#+&xxal#*`jNEod`bN<0~pRE5yfRDgQmBM3c!-_w&IzqFs_csMa>?x#H& z9-Czc;9<*`n2q_lZ426%msOmT++CjMR<;r@-{&Wk9cUDE8n&VRhATcZqrw+)GItJ# zANZj3p zaaT{t&b4=E9ob8(V{3cXd-6LEn|6(w`4D_?!Nuzoj3JU`YAN&+40^B`k7+)=DU}Fak;kg=iU8b zN8j4&|c1tgwOCx$r0ZQvReL$Sru2!YGH? z5Z(ZC5y~AH9Z-a5MAI-W0=kG86Aqd~mR+a%knu3OC6$=^y%;c0u0R}EAXtLub{&&X zF9;AojnyPAUWiewCL}~foCk|a{p?lvH}QUtP=#_)k%j5FHdn*`dO#0X6oLK;j2K>P zxT)Z^#AkrnOOCRp?bE6!hXaON^(F&6H}RUevpnh>b%HDFMBSJV1_;}k-))~@=xCZ3 z-f(KkL{tXa^AW>=Oo65hf-}Q4ot&A0(?@2y0qPCMbwGXU1q;@ZGSy(78vG6Q{0opW z!WaGR`tf2%XIA{h?(FW3P1{x{Z)fggvW{Y)C)-S)qQ48Ee{ppz@84PQ@67pkqEGhx zZC92rHF2F?X3s2o)5ipQGLmdPmT-WuiAi}6ZmKrX`x8hi!vl@r1f_uVWxLl(a#6-; zB(i+>?aH6w&)`LapEg~8xDIrnm6t(MyS`=NRr~fKTB-verg_WtoW}S4i)-iC`PC7) zu!qc&dde1rBo4`6*-K<65!wOlOhnZTS29KsywMU0!6zrzJQPxnQzKfi;h@T3n`zCM z#j1_+8a3c2jb?NVT@T4odERivLi3_}O*z5puENuh)nwFE0OceN%XXh45O#D;Qnh$w z4pZ&V2qS}HwH+F4n!Iflix%i91or4HFlTo{dUMh1Upb}oUwF8VK=#`21O1-{`q#Vj zf#E`6IN!d%(7s=Hl{#Ean@ZfbkuTb~*7j^yw(EA_PGD(#WnAY=UM{fd&QCE#dk2)K zTXz+`tvX+9^JRONWnC`%p>7o&Tbs=L_v*(tx_iu>`0(O`Jr4&S4m>*l@!%(e`N7u< zgRi3|-*dVE+I_mveOf;S-u08&z-l1x6$)M<=M^wRwteZ8ejIx0^PBUO1kNw$e^o## zBtY?or{A=rv{jXhyf=D&`V!=2wV<$)EIV57wOO6{Mw^&%!eY5#Y3yYjc`NNQvkMuw zR_BmYCQ+lA%NI*_Suo_=tr0RPNP4B+Kervh2;ixHY~&~Z3hymtRFnclae3FhDu7q+}Y+ zamb{}F|FPZFiWV$lS){&S|YQIWm#nr$w?(XZ#91ETs#?t>5*ab5G^26%Ix34L1siN z7O%=29k_lnkYxrJEdxx_P~&C3V5Z9}EX#D_jf*5#wgeKcb^7law<)mO^nM^3k0(@NWDKIG*iL0p zA*qO&yk&NNgxXTjS2q)}I6!_uTv@P0d8{$LcrY~$!!X4g853>@N$mSU>oNsFj79}` z6iDBj(m`Tf6{L8KoU=hDquS0WTX9NEtNw(d#Q!~0sQd`SeWwYeoKem8S54r*QQtZf z*gu}@AOGm!gOLZ~di(nM``vhK_?ie==lw8r{2yELKZ*T6G_XH9nH!z__~OI!k9aKM zt&a}jvEgf4aEC%(2i_l1(u}Z?S?8dK*=0oc3^)vzslJAT5_X1Nn@<={%F!#zdFpMu z8mmj7Rs0Wf4H`@?3I7p_)qes(hB{LhpFaJFcHXqW{mc z2bacI4{YomH3jg`7k}6HyJY^gONG}ip(H;#SpeNTS=c+7^G>46!BzhD*y^rT<=q#v z$MW8tYZr?Cj*7KpeCXhXKF`|7YOMJg76=>ptW~tv`^%CY7O2j~rtS6isnKRyedq7C zdlKF|RP2#uR*L!RXLY0-{PJ%f8%a655j96a7a{T(r)cLo2^u!8MJbK8&JcdYa4Bda3P%Q7Atz9t0NYv*2}5Gd$0o$|}LSN;;j z;7=%T(&&T_WHrhG#y&hYym2cFn6zKL0fNl97iTtmAbVittj=$2>obM;8zhr{D?J+f zIQmI6KX$e-b{6INzVQO+w(-KY@tkKI8CW%I)77PxYCFg}tdZ^<3$ox)CA-0A}uu;7=hDu{ez}WX5HbzhN)G zyKuwuZHthT`6<~vPEUy=vW;tMUOZm(G?fW*`yHK&XNo@m;_D{q>Rvoo>#kIp?{kNw2ww7PEc0n>Yp7FM zo1qYMMVOu%*e>`R*hN=-`YPVLGf_6k_$$v5bF+^rQE)>xTKzkalAY)IB6lLkoq)J_ z|DumRMXq&`eI9eWbJlN>8_Ze1MQ(r2`o+dTRCZglZP8t7yKLtnr{~k`*ypU~kKa(w GWclCU!DZ3%g*+v{Sr~*8YqkOEDGFx|tY6b09wCGVSNCPNP6l#eG9P(I$R;G&-#k?^iX!T>gM_~)E^&Wv(&SpY0WY?_Yx+oK%at_5Y( z(fsx(N0$ZAO=4%>Sgwy#&K%Pobu_WuyF>#Kaq6)|LXUPsQyB(TTc~z_ms=F$xo~m2*zC1S7 z_rIwgAG7NDCfy2u!qRKv3^=9Yt#}?wC*t+FNX4c4E50&BeY`hE%>GVaLd-M0`f54Km%pvGJvfzY?gB3SaRLcE#W>aO{rs?$n?fsszjMNR?sU zRaUEX+a8B^fZ4?dj+K{1u(iPa=sWH)MD`!&^yX_-3Hwz9^;AW|PliYJGK_OlY3nQ- z;q=QhsFFnz?O!PSy6<_5TTgeFC+Oirh-p4}9blVeNrl|>R5_JYI2`i6Ys(7~T$%rH zM14daxh&V%xW(xPE@@eQu@qY^tZ6Z0O)1ertVj!LOvxMiN=}Qxo)jx?n5%_+Vr*g} zX6UANv8bePD_PBmIh$6D?N)`Dy=%pKyB6$7FQjBnH_UWltvFqNeNEBxW64}04g1wJ zR65Ke_+fktphotBqxSc~%%)aX?&;lz1K>#u-=$?ihEnz-VkrmgtBqyXoAR;j;NzZv z-+>J94s5*Jm>*v+KR3uW$B`{b!Q4UfGb@Ty#zcpeGGa{l7&TcE-(=@@Z2qn~8sC+>l zzmsuF*L)VaFeJ3g>lb~P^5tS=4m`qRGE#N^^lGsVGCF4Su zEFK&OFeVVj$I)(QyBuB|-HA_(Sv}yUYd})pS53r=v;>`8MnbeQpTa1P*w+YXHW?HVyD#f>- z<-V~n^X}ZD#lJT*YY6~xh6(yzU@1??aUS^~Zg>1ALS;Ab4d22~?ZgW;{8e!TqA^6$?7_KiQpeiQpV(ilqY4<)|%pb?ze z56;x)pZbEEnR>e68`}2`)h;&!p{=X6xo2!2`18O+`pMv*24NFu1V{ISqqX^G(y7{y zn*P(Z*`~iAAkg>mwU4gZZ`98RKBJEY9}mJ^V<52)7)a~~616$Us-9^C&g}=z)#jf1 z!_81XeA^-)a6cyihX}RVwnRj!HrFQoexZGVc>K@b==b}dlYTL9hya8b`09J)t*O5) zH4<0%6Ia_LBzhP$ys=Hb5sX1R*$3;NxtreLrr0K8Solf;rDh;pUw?Sz3wJZvU%#@I z>RbY+>zB3`n&G~Byq>xLgL}Vv@XJO>2DTXt*WLGi-VS>`;3L2x!gGHx47Wj_=MbUY zgKs;o^L_zB5=96}gz*7j{Jw$uPws7OZL|p|Ugd1uAf?To(w-pe>wc*T;t4q zBQ)ReUfK6v*<;^lf)5kofrNxGAz=?D#DfU|0OBaH{S22bE{9YqDq5aYN-d;QDS8ps z1^qq(t`fAtWex<}YjGd6H(76Tt)P~2+BBVqHazGUDS$T5aa@yJ-y_#yk~pd6!%sW( z!;q^@I#83^mtAul2W}lfIKc^l&GO&*!99MkDR^tMzn;H4Ut^y(PlO?8W{&?GZVGgk literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/crypto/scrypt/__pycache__/_salsa.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/crypto/scrypt/__pycache__/_salsa.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d258c9e9b51823e93f8010f9f7a6fd77b672e4af GIT binary patch literal 4712 zcmdT`O=ufe5Z=|w*8iVnTmJXjN*YoU%UY%RqqIbZ(w4MoFf^?xxRRwRB8nt-R|^*J z5g&Sxds0r-srb~&HywKL(Z?LUH{+9`C*4$h3k7#Zvcf*%XxC{<>B2m{x0-Ku=FRVK zKA)R|=a<~?xkuw1_Xi&O4}Z2f3Bu-QF2k*H8U9CZjnCLtxvN6;i*ia)3iMa>tK zC1pe2EUQcFN~)lw#Oq@3S6YtU7B@77*?(!gGf3GKg6+OC}ja2oSz5!hePcFo+NtCnF4E3J`YeVIGH991~G49$2^%Q(+nbPVn>*S$vlIInAi~^5fWw)( zQlrnYuNvY>MjRY;UA^HBpFasr$Apm$ZBa zMv8o~Tv6R_5uUp#Ij8hq-0l5{g}kD|BvMpU`C`796XQ3FFuohPi-i!(f2NXIvXb1+E?G9UWX7 zB7l}_%beW zTqIl)xZJ=6xHXWXffNm-Xdp!cDH^^9DH=%8K#B%ZG?b#D6b+?lC`Cgl8c5;uN4%;; zK|{Q4ljr5vvB#yS*_iS)Rgp_E@K+|5Dk}MOAsd7FF;?DGAD4=WrFcB1sCA3!eqI=5z%8T%U^ZuU#H}n~ACaE*b zx!ha#4&9zw>Zi-wmydmcdUX4n#+2@j*7!rGyUy1?f9~Ij>w!znP&2(7(fx7V8L!y@ z=?~Tyw;wg!x_9xSyq`>C#eVDYsOo)-CKGfp*s^b0p*73(m6Uvdf;+%p(*cP z(fyL{l-`vN_w-<2ZlEjoEp#~V&d%4DwXJ^-Z_**L{cYoY-FwN({aV+7^;=(uH~He9 z>R=^y%yclu!(d+4wtp*r`*P3s@%cJjgxAWQ^>w%i??rStpZEVAzx~*;l5cOrYbA&J za$n~?cDia0w})$dXT{EWyuFg#`2_a9n!J6bzNWk8w~~i$?|*sn`0L^AJ5crAi(4y) z@8ooh_SL1j+>jdbjf5@#KaH}2n_cs13zQeuwrz`Jo DtGW5g literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/crypto/scrypt/_builtin.py b/ansible/lib/python3.11/site-packages/passlib/crypto/scrypt/_builtin.py new file mode 100644 index 000000000..e9bb305d2 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/crypto/scrypt/_builtin.py @@ -0,0 +1,244 @@ +"""passlib.utils.scrypt._builtin -- scrypt() kdf in pure-python""" +#========================================================================== +# imports +#========================================================================== +# core +import operator +import struct +# pkg +from passlib.utils.compat import izip +from passlib.crypto.digest import pbkdf2_hmac +from passlib.crypto.scrypt._salsa import salsa20 +# local +__all__ =[ + "ScryptEngine", +] + +#========================================================================== +# scrypt engine +#========================================================================== +class ScryptEngine(object): + """ + helper class used to run scrypt kdf, see scrypt() for frontend + + .. warning:: + this class does NO validation of the input ranges or types. + + it's not intended to be used directly, + but only as a backend for :func:`passlib.utils.scrypt.scrypt()`. + """ + #================================================================= + # instance attrs + #================================================================= + + # primary scrypt config parameters + n = 0 + r = 0 + p = 0 + + # derived values & objects + smix_bytes = 0 + iv_bytes = 0 + bmix_len = 0 + bmix_half_len = 0 + bmix_struct = None + integerify = None + + #================================================================= + # frontend + #================================================================= + @classmethod + def execute(cls, secret, salt, n, r, p, keylen): + """create engine & run scrypt() hash calculation""" + return cls(n, r, p).run(secret, salt, keylen) + + #================================================================= + # init + #================================================================= + def __init__(self, n, r, p): + # store config + self.n = n + self.r = r + self.p = p + self.smix_bytes = r << 7 # num bytes in smix input - 2*r*16*4 + self.iv_bytes = self.smix_bytes * p + self.bmix_len = bmix_len = r << 5 # length of bmix block list - 32*r integers + self.bmix_half_len = r << 4 + assert struct.calcsize("I") == 4 + self.bmix_struct = struct.Struct("<" + str(bmix_len) + "I") + + # use optimized bmix for certain cases + if r == 1: + self.bmix = self._bmix_1 + + # pick best integerify function - integerify(bmix_block) should + # take last 64 bytes of block and return a little-endian integer. + # since it's immediately converted % n, we only have to extract + # the first 32 bytes if n < 2**32 - which due to the current + # internal representation, is already unpacked as a 32-bit int. + if n <= 0xFFFFffff: + integerify = operator.itemgetter(-16) + else: + assert n <= 0xFFFFffffFFFFffff + ig1 = operator.itemgetter(-16) + ig2 = operator.itemgetter(-17) + def integerify(X): + return ig1(X) | (ig2(X)<<32) + self.integerify = integerify + + #================================================================= + # frontend + #================================================================= + def run(self, secret, salt, keylen): + """ + run scrypt kdf for specified secret, salt, and keylen + + .. note:: + + * time cost is ``O(n * r * p)`` + * mem cost is ``O(n * r)`` + """ + # stretch salt into initial byte array via pbkdf2 + iv_bytes = self.iv_bytes + input = pbkdf2_hmac("sha256", secret, salt, rounds=1, keylen=iv_bytes) + + # split initial byte array into 'p' mflen-sized chunks, + # and run each chunk through smix() to generate output chunk. + smix = self.smix + if self.p == 1: + output = smix(input) + else: + # XXX: *could* use threading here, if really high p values encountered, + # but would tradeoff for more memory usage. + smix_bytes = self.smix_bytes + output = b''.join( + smix(input[offset:offset+smix_bytes]) + for offset in range(0, iv_bytes, smix_bytes) + ) + + # stretch final byte array into output via pbkdf2 + return pbkdf2_hmac("sha256", secret, output, rounds=1, keylen=keylen) + + #================================================================= + # smix() helper + #================================================================= + def smix(self, input): + """run SCrypt smix function on a single input block + + :arg input: + byte string containing input data. + interpreted as 32*r little endian 4 byte integers. + + :returns: + byte string containing output data + derived by mixing input using n & r parameters. + + .. note:: time & mem cost are both ``O(n * r)`` + """ + # gather locals + bmix = self.bmix + bmix_struct = self.bmix_struct + integerify = self.integerify + n = self.n + + # parse input into 32*r integers ('X' in scrypt source) + # mem cost -- O(r) + buffer = list(bmix_struct.unpack(input)) + + # starting with initial buffer contents, derive V s.t. + # V[0]=initial_buffer ... V[i] = bmix(V[i-1], V[i-1]) ... V[n-1] = bmix(V[n-2], V[n-2]) + # final buffer contents should equal bmix(V[n-1], V[n-1]) + # + # time cost -- O(n * r) -- n loops, bmix is O(r) + # mem cost -- O(n * r) -- V is n-element array of r-element tuples + # NOTE: could do time / memory tradeoff to shrink size of V + def vgen(): + i = 0 + while i < n: + last = tuple(buffer) + yield last + bmix(last, buffer) + i += 1 + V = list(vgen()) + + # generate result from X & V. + # + # time cost -- O(n * r) -- loops n times, calls bmix() which has O(r) time cost + # mem cost -- O(1) -- allocates nothing, calls bmix() which has O(1) mem cost + get_v_elem = V.__getitem__ + n_mask = n - 1 + i = 0 + while i < n: + j = integerify(buffer) & n_mask + result = tuple(a ^ b for a, b in izip(buffer, get_v_elem(j))) + bmix(result, buffer) + i += 1 + + # # NOTE: we could easily support arbitrary values of ``n``, not just powers of 2, + # # but very few implementations have that ability, so not enabling it for now... + # if not n_is_log_2: + # while i < n: + # j = integerify(buffer) % n + # tmp = tuple(a^b for a,b in izip(buffer, get_v_elem(j))) + # bmix(tmp,buffer) + # i += 1 + + # repack tmp + return bmix_struct.pack(*buffer) + + #================================================================= + # bmix() helper + #================================================================= + def bmix(self, source, target): + """ + block mixing function used by smix() + uses salsa20/8 core to mix block contents. + + :arg source: + source to read from. + should be list of 32*r 4-byte integers + (2*r salsa20 blocks). + + :arg target: + target to write to. + should be list with same size as source. + the existing value of this buffer is ignored. + + .. warning:: + + this operates *in place* on target, + so source & target should NOT be same list. + + .. note:: + + * time cost is ``O(r)`` -- loops 16*r times, salsa20() has ``O(1)`` cost. + + * memory cost is ``O(1)`` -- salsa20() uses 16 x uint4, + all other operations done in-place. + """ + ## assert source is not target + # Y[-1] = B[2r-1], Y[i] = hash( Y[i-1] xor B[i]) + # B' <-- (Y_0, Y_2 ... Y_{2r-2}, Y_1, Y_3 ... Y_{2r-1}) */ + half = self.bmix_half_len # 16*r out of 32*r - start of Y_1 + tmp = source[-16:] # 'X' in scrypt source + siter = iter(source) + j = 0 + while j < half: + jn = j+16 + target[j:jn] = tmp = salsa20(a ^ b for a, b in izip(tmp, siter)) + target[half+j:half+jn] = tmp = salsa20(a ^ b for a, b in izip(tmp, siter)) + j = jn + + def _bmix_1(self, source, target): + """special bmix() method optimized for ``r=1`` case""" + B = source[16:] + target[:16] = tmp = salsa20(a ^ b for a, b in izip(B, iter(source))) + target[16:] = salsa20(a ^ b for a, b in izip(tmp, B)) + + #================================================================= + # eoc + #================================================================= + +#========================================================================== +# eof +#========================================================================== diff --git a/ansible/lib/python3.11/site-packages/passlib/crypto/scrypt/_gen_files.py b/ansible/lib/python3.11/site-packages/passlib/crypto/scrypt/_gen_files.py new file mode 100644 index 000000000..55ddfae3b --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/crypto/scrypt/_gen_files.py @@ -0,0 +1,154 @@ +"""passlib.utils.scrypt._gen_files - meta script that generates _salsa.py""" +#========================================================================== +# imports +#========================================================================== +# core +import os +# pkg +# local +#========================================================================== +# constants +#========================================================================== + +_SALSA_OPS = [ + # row = (target idx, source idx 1, source idx 2, rotate) + # interpreted as salsa operation over uint32... + # target = (source1+source2)<> (32 - (b)))) + ##x[ 4] ^= R(x[ 0]+x[12], 7); x[ 8] ^= R(x[ 4]+x[ 0], 9); + ##x[12] ^= R(x[ 8]+x[ 4],13); x[ 0] ^= R(x[12]+x[ 8],18); + ( 4, 0, 12, 7), + ( 8, 4, 0, 9), + ( 12, 8, 4, 13), + ( 0, 12, 8, 18), + + ##x[ 9] ^= R(x[ 5]+x[ 1], 7); x[13] ^= R(x[ 9]+x[ 5], 9); + ##x[ 1] ^= R(x[13]+x[ 9],13); x[ 5] ^= R(x[ 1]+x[13],18); + ( 9, 5, 1, 7), + ( 13, 9, 5, 9), + ( 1, 13, 9, 13), + ( 5, 1, 13, 18), + + ##x[14] ^= R(x[10]+x[ 6], 7); x[ 2] ^= R(x[14]+x[10], 9); + ##x[ 6] ^= R(x[ 2]+x[14],13); x[10] ^= R(x[ 6]+x[ 2],18); + ( 14, 10, 6, 7), + ( 2, 14, 10, 9), + ( 6, 2, 14, 13), + ( 10, 6, 2, 18), + + ##x[ 3] ^= R(x[15]+x[11], 7); x[ 7] ^= R(x[ 3]+x[15], 9); + ##x[11] ^= R(x[ 7]+x[ 3],13); x[15] ^= R(x[11]+x[ 7],18); + ( 3, 15, 11, 7), + ( 7, 3, 15, 9), + ( 11, 7, 3, 13), + ( 15, 11, 7, 18), + + ##/* Operate on rows. */ + ##x[ 1] ^= R(x[ 0]+x[ 3], 7); x[ 2] ^= R(x[ 1]+x[ 0], 9); + ##x[ 3] ^= R(x[ 2]+x[ 1],13); x[ 0] ^= R(x[ 3]+x[ 2],18); + ( 1, 0, 3, 7), + ( 2, 1, 0, 9), + ( 3, 2, 1, 13), + ( 0, 3, 2, 18), + + ##x[ 6] ^= R(x[ 5]+x[ 4], 7); x[ 7] ^= R(x[ 6]+x[ 5], 9); + ##x[ 4] ^= R(x[ 7]+x[ 6],13); x[ 5] ^= R(x[ 4]+x[ 7],18); + ( 6, 5, 4, 7), + ( 7, 6, 5, 9), + ( 4, 7, 6, 13), + ( 5, 4, 7, 18), + + ##x[11] ^= R(x[10]+x[ 9], 7); x[ 8] ^= R(x[11]+x[10], 9); + ##x[ 9] ^= R(x[ 8]+x[11],13); x[10] ^= R(x[ 9]+x[ 8],18); + ( 11, 10, 9, 7), + ( 8, 11, 10, 9), + ( 9, 8, 11, 13), + ( 10, 9, 8, 18), + + ##x[12] ^= R(x[15]+x[14], 7); x[13] ^= R(x[12]+x[15], 9); + ##x[14] ^= R(x[13]+x[12],13); x[15] ^= R(x[14]+x[13],18); + ( 12, 15, 14, 7), + ( 13, 12, 15, 9), + ( 14, 13, 12, 13), + ( 15, 14, 13, 18), +] + +def main(): + target = os.path.join(os.path.dirname(__file__), "_salsa.py") + fh = file(target, "w") + write = fh.write + + VNAMES = ["v%d" % i for i in range(16)] + + PAD = " " * 4 + PAD2 = " " * 8 + PAD3 = " " * 12 + TLIST = ", ".join("b%d" % i for i in range(16)) + VLIST = ", ".join(VNAMES) + kwds = dict( + VLIST=VLIST, + TLIST=TLIST, + ) + + write('''\ +"""passlib.utils.scrypt._salsa - salsa 20/8 core, autogenerated by _gen_salsa.py""" +#================================================================= +# salsa function +#================================================================= + +def salsa20(input): + \"""apply the salsa20/8 core to the provided input + + :args input: input list containing 16 32-bit integers + :returns: result list containing 16 32-bit integers + \""" + + %(TLIST)s = input + %(VLIST)s = \\ + %(TLIST)s + + i = 0 + while i < 4: +''' % kwds) + + for idx, (target, source1, source2, rotate) in enumerate(_SALSA_OPS): + write('''\ + # salsa op %(idx)d: [%(it)d] ^= ([%(is1)d]+[%(is2)d])<<<%(rot1)d + t = (%(src1)s + %(src2)s) & 0xffffffff + %(dst)s ^= ((t & 0x%(rmask)08x) << %(rot1)d) | (t >> %(rot2)d) + +''' % dict( + idx=idx, is1 = source1, is2=source2, it=target, + src1=VNAMES[source1], + src2=VNAMES[source2], + dst=VNAMES[target], + rmask=(1<<(32-rotate))-1, + rot1=rotate, + rot2=32-rotate, + )) + + write('''\ + i += 1 + +''') + + for idx in range(16): + write(PAD + "b%d = (b%d + v%d) & 0xffffffff\n" % (idx,idx,idx)) + + write('''\ + + return %(TLIST)s + +#================================================================= +# eof +#================================================================= +''' % kwds) + +if __name__ == "__main__": + main() + +#========================================================================== +# eof +#========================================================================== diff --git a/ansible/lib/python3.11/site-packages/passlib/crypto/scrypt/_salsa.py b/ansible/lib/python3.11/site-packages/passlib/crypto/scrypt/_salsa.py new file mode 100644 index 000000000..9112732e8 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/crypto/scrypt/_salsa.py @@ -0,0 +1,170 @@ +"""passlib.utils.scrypt._salsa - salsa 20/8 core, autogenerated by _gen_salsa.py""" +#================================================================= +# salsa function +#================================================================= + +def salsa20(input): + """apply the salsa20/8 core to the provided input + + :args input: input list containing 16 32-bit integers + :returns: result list containing 16 32-bit integers + """ + + b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15 = input + v0, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15 = \ + b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15 + + i = 0 + while i < 4: + # salsa op 0: [4] ^= ([0]+[12])<<<7 + t = (v0 + v12) & 0xffffffff + v4 ^= ((t & 0x01ffffff) << 7) | (t >> 25) + + # salsa op 1: [8] ^= ([4]+[0])<<<9 + t = (v4 + v0) & 0xffffffff + v8 ^= ((t & 0x007fffff) << 9) | (t >> 23) + + # salsa op 2: [12] ^= ([8]+[4])<<<13 + t = (v8 + v4) & 0xffffffff + v12 ^= ((t & 0x0007ffff) << 13) | (t >> 19) + + # salsa op 3: [0] ^= ([12]+[8])<<<18 + t = (v12 + v8) & 0xffffffff + v0 ^= ((t & 0x00003fff) << 18) | (t >> 14) + + # salsa op 4: [9] ^= ([5]+[1])<<<7 + t = (v5 + v1) & 0xffffffff + v9 ^= ((t & 0x01ffffff) << 7) | (t >> 25) + + # salsa op 5: [13] ^= ([9]+[5])<<<9 + t = (v9 + v5) & 0xffffffff + v13 ^= ((t & 0x007fffff) << 9) | (t >> 23) + + # salsa op 6: [1] ^= ([13]+[9])<<<13 + t = (v13 + v9) & 0xffffffff + v1 ^= ((t & 0x0007ffff) << 13) | (t >> 19) + + # salsa op 7: [5] ^= ([1]+[13])<<<18 + t = (v1 + v13) & 0xffffffff + v5 ^= ((t & 0x00003fff) << 18) | (t >> 14) + + # salsa op 8: [14] ^= ([10]+[6])<<<7 + t = (v10 + v6) & 0xffffffff + v14 ^= ((t & 0x01ffffff) << 7) | (t >> 25) + + # salsa op 9: [2] ^= ([14]+[10])<<<9 + t = (v14 + v10) & 0xffffffff + v2 ^= ((t & 0x007fffff) << 9) | (t >> 23) + + # salsa op 10: [6] ^= ([2]+[14])<<<13 + t = (v2 + v14) & 0xffffffff + v6 ^= ((t & 0x0007ffff) << 13) | (t >> 19) + + # salsa op 11: [10] ^= ([6]+[2])<<<18 + t = (v6 + v2) & 0xffffffff + v10 ^= ((t & 0x00003fff) << 18) | (t >> 14) + + # salsa op 12: [3] ^= ([15]+[11])<<<7 + t = (v15 + v11) & 0xffffffff + v3 ^= ((t & 0x01ffffff) << 7) | (t >> 25) + + # salsa op 13: [7] ^= ([3]+[15])<<<9 + t = (v3 + v15) & 0xffffffff + v7 ^= ((t & 0x007fffff) << 9) | (t >> 23) + + # salsa op 14: [11] ^= ([7]+[3])<<<13 + t = (v7 + v3) & 0xffffffff + v11 ^= ((t & 0x0007ffff) << 13) | (t >> 19) + + # salsa op 15: [15] ^= ([11]+[7])<<<18 + t = (v11 + v7) & 0xffffffff + v15 ^= ((t & 0x00003fff) << 18) | (t >> 14) + + # salsa op 16: [1] ^= ([0]+[3])<<<7 + t = (v0 + v3) & 0xffffffff + v1 ^= ((t & 0x01ffffff) << 7) | (t >> 25) + + # salsa op 17: [2] ^= ([1]+[0])<<<9 + t = (v1 + v0) & 0xffffffff + v2 ^= ((t & 0x007fffff) << 9) | (t >> 23) + + # salsa op 18: [3] ^= ([2]+[1])<<<13 + t = (v2 + v1) & 0xffffffff + v3 ^= ((t & 0x0007ffff) << 13) | (t >> 19) + + # salsa op 19: [0] ^= ([3]+[2])<<<18 + t = (v3 + v2) & 0xffffffff + v0 ^= ((t & 0x00003fff) << 18) | (t >> 14) + + # salsa op 20: [6] ^= ([5]+[4])<<<7 + t = (v5 + v4) & 0xffffffff + v6 ^= ((t & 0x01ffffff) << 7) | (t >> 25) + + # salsa op 21: [7] ^= ([6]+[5])<<<9 + t = (v6 + v5) & 0xffffffff + v7 ^= ((t & 0x007fffff) << 9) | (t >> 23) + + # salsa op 22: [4] ^= ([7]+[6])<<<13 + t = (v7 + v6) & 0xffffffff + v4 ^= ((t & 0x0007ffff) << 13) | (t >> 19) + + # salsa op 23: [5] ^= ([4]+[7])<<<18 + t = (v4 + v7) & 0xffffffff + v5 ^= ((t & 0x00003fff) << 18) | (t >> 14) + + # salsa op 24: [11] ^= ([10]+[9])<<<7 + t = (v10 + v9) & 0xffffffff + v11 ^= ((t & 0x01ffffff) << 7) | (t >> 25) + + # salsa op 25: [8] ^= ([11]+[10])<<<9 + t = (v11 + v10) & 0xffffffff + v8 ^= ((t & 0x007fffff) << 9) | (t >> 23) + + # salsa op 26: [9] ^= ([8]+[11])<<<13 + t = (v8 + v11) & 0xffffffff + v9 ^= ((t & 0x0007ffff) << 13) | (t >> 19) + + # salsa op 27: [10] ^= ([9]+[8])<<<18 + t = (v9 + v8) & 0xffffffff + v10 ^= ((t & 0x00003fff) << 18) | (t >> 14) + + # salsa op 28: [12] ^= ([15]+[14])<<<7 + t = (v15 + v14) & 0xffffffff + v12 ^= ((t & 0x01ffffff) << 7) | (t >> 25) + + # salsa op 29: [13] ^= ([12]+[15])<<<9 + t = (v12 + v15) & 0xffffffff + v13 ^= ((t & 0x007fffff) << 9) | (t >> 23) + + # salsa op 30: [14] ^= ([13]+[12])<<<13 + t = (v13 + v12) & 0xffffffff + v14 ^= ((t & 0x0007ffff) << 13) | (t >> 19) + + # salsa op 31: [15] ^= ([14]+[13])<<<18 + t = (v14 + v13) & 0xffffffff + v15 ^= ((t & 0x00003fff) << 18) | (t >> 14) + + i += 1 + + b0 = (b0 + v0) & 0xffffffff + b1 = (b1 + v1) & 0xffffffff + b2 = (b2 + v2) & 0xffffffff + b3 = (b3 + v3) & 0xffffffff + b4 = (b4 + v4) & 0xffffffff + b5 = (b5 + v5) & 0xffffffff + b6 = (b6 + v6) & 0xffffffff + b7 = (b7 + v7) & 0xffffffff + b8 = (b8 + v8) & 0xffffffff + b9 = (b9 + v9) & 0xffffffff + b10 = (b10 + v10) & 0xffffffff + b11 = (b11 + v11) & 0xffffffff + b12 = (b12 + v12) & 0xffffffff + b13 = (b13 + v13) & 0xffffffff + b14 = (b14 + v14) & 0xffffffff + b15 = (b15 + v15) & 0xffffffff + + return b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15 + +#================================================================= +# eof +#================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/exc.py b/ansible/lib/python3.11/site-packages/passlib/exc.py new file mode 100644 index 000000000..755c7dcca --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/exc.py @@ -0,0 +1,397 @@ +"""passlib.exc -- exceptions & warnings raised by passlib""" +#============================================================================= +# exceptions +#============================================================================= +class UnknownBackendError(ValueError): + """ + Error raised if multi-backend handler doesn't recognize backend name. + Inherits from :exc:`ValueError`. + + .. versionadded:: 1.7 + """ + def __init__(self, hasher, backend): + self.hasher = hasher + self.backend = backend + message = "%s: unknown backend: %r" % (hasher.name, backend) + ValueError.__init__(self, message) + + +# XXX: add a PasslibRuntimeError as base for Missing/Internal/Security runtime errors? + + +class MissingBackendError(RuntimeError): + """Error raised if multi-backend handler has no available backends; + or if specifically requested backend is not available. + + :exc:`!MissingBackendError` derives + from :exc:`RuntimeError`, since it usually indicates + lack of an external library or OS feature. + This is primarily raised by handlers which depend on + external libraries (which is currently just + :class:`~passlib.hash.bcrypt`). + """ + + +class InternalBackendError(RuntimeError): + """ + Error raised if something unrecoverable goes wrong with backend call; + such as if ``crypt.crypt()`` returning a malformed hash. + + .. versionadded:: 1.7.3 + """ + + +class PasswordValueError(ValueError): + """ + Error raised if a password can't be hashed / verified for various reasons. + This exception derives from the builtin :exc:`!ValueError`. + + May be thrown directly when password violates internal invariants of hasher + (e.g. some don't support NULL characters). Hashers may also throw more specific subclasses, + such as :exc:`!PasswordSizeError`. + + .. versionadded:: 1.7.3 + """ + pass + + +class PasswordSizeError(PasswordValueError): + """ + Error raised if a password exceeds the maximum size allowed + by Passlib (by default, 4096 characters); or if password exceeds + a hash-specific size limitation. + + This exception derives from :exc:`PasswordValueError` (above). + + Many password hash algorithms take proportionately larger amounts of time and/or + memory depending on the size of the password provided. This could present + a potential denial of service (DOS) situation if a maliciously large + password is provided to an application. Because of this, Passlib enforces + a maximum size limit, but one which should be *much* larger + than any legitimate password. :exc:`PasswordSizeError` derives + from :exc:`!ValueError`. + + .. note:: + Applications wishing to use a different limit should set the + ``PASSLIB_MAX_PASSWORD_SIZE`` environmental variable before + Passlib is loaded. The value can be any large positive integer. + + .. attribute:: max_size + + indicates the maximum allowed size. + + .. versionadded:: 1.6 + """ + + max_size = None + + def __init__(self, max_size, msg=None): + self.max_size = max_size + if msg is None: + msg = "password exceeds maximum allowed size" + PasswordValueError.__init__(self, msg) + + # this also prevents a glibc crypt segfault issue, detailed here ... + # http://www.openwall.com/lists/oss-security/2011/11/15/1 + +class PasswordTruncateError(PasswordSizeError): + """ + Error raised if password would be truncated by hash. + This derives from :exc:`PasswordSizeError` (above). + + Hashers such as :class:`~passlib.hash.bcrypt` can be configured to raises + this error by setting ``truncate_error=True``. + + .. attribute:: max_size + + indicates the maximum allowed size. + + .. versionadded:: 1.7 + """ + + def __init__(self, cls, msg=None): + if msg is None: + msg = ("Password too long (%s truncates to %d characters)" % + (cls.name, cls.truncate_size)) + PasswordSizeError.__init__(self, cls.truncate_size, msg) + + +class PasslibSecurityError(RuntimeError): + """ + Error raised if critical security issue is detected + (e.g. an attempt is made to use a vulnerable version of a bcrypt backend). + + .. versionadded:: 1.6.3 + """ + + +class TokenError(ValueError): + """ + Base error raised by v:mod:`passlib.totp` when + a token can't be parsed / isn't valid / etc. + Derives from :exc:`!ValueError`. + + Usually one of the more specific subclasses below will be raised: + + * :class:`MalformedTokenError` -- invalid chars, too few digits + * :class:`InvalidTokenError` -- no match found + * :class:`UsedTokenError` -- match found, but token already used + + .. versionadded:: 1.7 + """ + + #: default message to use if none provided -- subclasses may fill this in + _default_message = 'Token not acceptable' + + def __init__(self, msg=None, *args, **kwds): + if msg is None: + msg = self._default_message + ValueError.__init__(self, msg, *args, **kwds) + + +class MalformedTokenError(TokenError): + """ + Error raised by :mod:`passlib.totp` when a token isn't formatted correctly + (contains invalid characters, wrong number of digits, etc) + """ + _default_message = "Unrecognized token" + + +class InvalidTokenError(TokenError): + """ + Error raised by :mod:`passlib.totp` when a token is formatted correctly, + but doesn't match any tokens within valid range. + """ + _default_message = "Token did not match" + + +class UsedTokenError(TokenError): + """ + Error raised by :mod:`passlib.totp` if a token is reused. + Derives from :exc:`TokenError`. + + .. autoattribute:: expire_time + + .. versionadded:: 1.7 + """ + _default_message = "Token has already been used, please wait for another." + + #: optional value indicating when current counter period will end, + #: and a new token can be generated. + expire_time = None + + def __init__(self, *args, **kwds): + self.expire_time = kwds.pop("expire_time", None) + TokenError.__init__(self, *args, **kwds) + + +class UnknownHashError(ValueError): + """ + Error raised by :class:`~passlib.crypto.lookup_hash` if hash name is not recognized. + This exception derives from :exc:`!ValueError`. + + As of version 1.7.3, this may also be raised if hash algorithm is known, + but has been disabled due to FIPS mode (message will include phrase "disabled for fips"). + + As of version 1.7.4, this may be raised if a :class:`~passlib.context.CryptContext` + is unable to identify the algorithm used by a password hash. + + .. versionadded:: 1.7 + + .. versionchanged: 1.7.3 + added 'message' argument. + + .. versionchanged:: 1.7.4 + altered call signature. + """ + def __init__(self, message=None, value=None): + self.value = value + if message is None: + message = "unknown hash algorithm: %r" % value + self.message = message + ValueError.__init__(self, message, value) + + def __str__(self): + return self.message + + +#============================================================================= +# warnings +#============================================================================= +class PasslibWarning(UserWarning): + """base class for Passlib's user warnings, + derives from the builtin :exc:`UserWarning`. + + .. versionadded:: 1.6 + """ + +# XXX: there's only one reference to this class, and it will go away in 2.0; +# so can probably remove this along with this / roll this into PasslibHashWarning. +class PasslibConfigWarning(PasslibWarning): + """Warning issued when non-fatal issue is found related to the configuration + of a :class:`~passlib.context.CryptContext` instance. + + This occurs primarily in one of two cases: + + * The CryptContext contains rounds limits which exceed the hard limits + imposed by the underlying algorithm. + * An explicit rounds value was provided which exceeds the limits + imposed by the CryptContext. + + In both of these cases, the code will perform correctly & securely; + but the warning is issued as a sign the configuration may need updating. + + .. versionadded:: 1.6 + """ + +class PasslibHashWarning(PasslibWarning): + """Warning issued when non-fatal issue is found with parameters + or hash string passed to a passlib hash class. + + This occurs primarily in one of two cases: + + * A rounds value or other setting was explicitly provided which + exceeded the handler's limits (and has been clamped + by the :ref:`relaxed` flag). + + * A malformed hash string was encountered which (while parsable) + should be re-encoded. + + .. versionadded:: 1.6 + """ + +class PasslibRuntimeWarning(PasslibWarning): + """Warning issued when something unexpected happens during runtime. + + The fact that it's a warning instead of an error means Passlib + was able to correct for the issue, but that it's anomalous enough + that the developers would love to hear under what conditions it occurred. + + .. versionadded:: 1.6 + """ + +class PasslibSecurityWarning(PasslibWarning): + """Special warning issued when Passlib encounters something + that might affect security. + + .. versionadded:: 1.6 + """ + +#============================================================================= +# error constructors +# +# note: these functions are used by the hashes in Passlib to raise common +# error messages. They are currently just functions which return ValueError, +# rather than subclasses of ValueError, since the specificity isn't needed +# yet; and who wants to import a bunch of error classes when catching +# ValueError will do? +#============================================================================= + +def _get_name(handler): + return handler.name if handler else "" + +#------------------------------------------------------------------------ +# generic helpers +#------------------------------------------------------------------------ +def type_name(value): + """return pretty-printed string containing name of value's type""" + cls = value.__class__ + if cls.__module__ and cls.__module__ not in ["__builtin__", "builtins"]: + return "%s.%s" % (cls.__module__, cls.__name__) + elif value is None: + return 'None' + else: + return cls.__name__ + +def ExpectedTypeError(value, expected, param): + """error message when param was supposed to be one type, but found another""" + # NOTE: value is never displayed, since it may sometimes be a password. + name = type_name(value) + return TypeError("%s must be %s, not %s" % (param, expected, name)) + +def ExpectedStringError(value, param): + """error message when param was supposed to be unicode or bytes""" + return ExpectedTypeError(value, "unicode or bytes", param) + +#------------------------------------------------------------------------ +# hash/verify parameter errors +#------------------------------------------------------------------------ +def MissingDigestError(handler=None): + """raised when verify() method gets passed config string instead of hash""" + name = _get_name(handler) + return ValueError("expected %s hash, got %s config string instead" % + (name, name)) + +def NullPasswordError(handler=None): + """raised by OS crypt() supporting hashes, which forbid NULLs in password""" + name = _get_name(handler) + return PasswordValueError("%s does not allow NULL bytes in password" % name) + +#------------------------------------------------------------------------ +# errors when parsing hashes +#------------------------------------------------------------------------ +def InvalidHashError(handler=None): + """error raised if unrecognized hash provided to handler""" + return ValueError("not a valid %s hash" % _get_name(handler)) + +def MalformedHashError(handler=None, reason=None): + """error raised if recognized-but-malformed hash provided to handler""" + text = "malformed %s hash" % _get_name(handler) + if reason: + text = "%s (%s)" % (text, reason) + return ValueError(text) + +def ZeroPaddedRoundsError(handler=None): + """error raised if hash was recognized but contained zero-padded rounds field""" + return MalformedHashError(handler, "zero-padded rounds") + +#------------------------------------------------------------------------ +# settings / hash component errors +#------------------------------------------------------------------------ +def ChecksumSizeError(handler, raw=False): + """error raised if hash was recognized, but checksum was wrong size""" + # TODO: if handler.use_defaults is set, this came from app-provided value, + # not from parsing a hash string, might want different error msg. + checksum_size = handler.checksum_size + unit = "bytes" if raw else "chars" + reason = "checksum must be exactly %d %s" % (checksum_size, unit) + return MalformedHashError(handler, reason) + +#============================================================================= +# sensitive info helpers +#============================================================================= + +#: global flag, set temporarily by UTs to allow debug_only_repr() to display sensitive values. +ENABLE_DEBUG_ONLY_REPR = False + + +def debug_only_repr(value, param="hash"): + """ + helper used to display sensitive data (hashes etc) within error messages. + currently returns placeholder test UNLESS unittests are running, + in which case the real value is displayed. + + mainly useful to prevent hashes / secrets from being exposed in production tracebacks; + while still being visible from test failures. + + NOTE: api subject to change, may formalize this more in the future. + """ + if ENABLE_DEBUG_ONLY_REPR or value is None or isinstance(value, bool): + return repr(value) + return "<%s %s value omitted>" % (param, type(value)) + + +def CryptBackendError(handler, config, hash, # * + source="crypt.crypt()"): + """ + helper to generate standard message when ``crypt.crypt()`` returns invalid result. + takes care of automatically masking contents of config & hash outside of UTs. + """ + name = _get_name(handler) + msg = "%s returned invalid %s hash: config=%s hash=%s" % \ + (source, name, debug_only_repr(config), debug_only_repr(hash)) + raise InternalBackendError(msg) + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/ext/__init__.py b/ansible/lib/python3.11/site-packages/passlib/ext/__init__.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/ext/__init__.py @@ -0,0 +1 @@ + diff --git a/ansible/lib/python3.11/site-packages/passlib/ext/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/ext/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..895142c08298e40390fbe78ad81186938171488a GIT binary patch literal 187 zcmZ3^%ge<81oP6jq%#8P#~=<2fCNC`GaHbY&XB?o%%I8Wx00cV2_y)T`Q@jdk)NBY zU!0p*T9mI}P?VpdpO{yinUs^NpOcxSUrT2Z1OAD@|*SrQ+wS5Wzj!zMRBr8Fniu80+AJjemX{6OLZGb1D82L>2X#0(Sz E067#dApigX literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/ext/django/__init__.py b/ansible/lib/python3.11/site-packages/passlib/ext/django/__init__.py new file mode 100644 index 000000000..2dc9b2821 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/ext/django/__init__.py @@ -0,0 +1,6 @@ +"""passlib.ext.django.models -- monkeypatch django hashing framework + +this plugin monkeypatches django's hashing framework +so that it uses a passlib context object, allowing handling of arbitrary +hashes in Django databases. +""" diff --git a/ansible/lib/python3.11/site-packages/passlib/ext/django/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/ext/django/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e866f4355efc2ed9821e1c7cfb0467e510db4351 GIT binary patch literal 437 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 (2, 1) + + #: django is_usable_password() started returning True for password = {None, ""} values. + empty_is_usable_password = DJANGO_VERSION >= (2, 1) + + #: django is_usable_password() started returning True for non-hash strings in 2.1 + invalid_is_usable_password = DJANGO_VERSION >= (2, 1) + +#============================================================================= +# default policies +#============================================================================= + +# map preset names -> passlib.app attrs +_preset_map = { + "django-1.0": "django10_context", + "django-1.4": "django14_context", + "django-1.6": "django16_context", + "django-latest": "django_context", +} + +def get_preset_config(name): + """Returns configuration string for one of the preset strings + supported by the ``PASSLIB_CONFIG`` setting. + Currently supported presets: + + * ``"passlib-default"`` - default config used by this release of passlib. + * ``"django-default"`` - config matching currently installed django version. + * ``"django-latest"`` - config matching newest django version (currently same as ``"django-1.6"``). + * ``"django-1.0"`` - config used by stock Django 1.0 - 1.3 installs + * ``"django-1.4"`` - config used by stock Django 1.4 installs + * ``"django-1.6"`` - config used by stock Django 1.6 installs + """ + # TODO: add preset which includes HASHERS + PREFERRED_HASHERS, + # after having imported any custom hashers. e.g. "django-current" + if name == "django-default": + if not DJANGO_VERSION: + raise ValueError("can't resolve django-default preset, " + "django not installed") + name = "django-1.6" + if name == "passlib-default": + return PASSLIB_DEFAULT + try: + attr = _preset_map[name] + except KeyError: + raise ValueError("unknown preset config name: %r" % name) + import passlib.apps + return getattr(passlib.apps, attr).to_string() + +# default context used by passlib 1.6 +PASSLIB_DEFAULT = """ +[passlib] + +; list of schemes supported by configuration +; currently all django 1.6, 1.4, and 1.0 hashes, +; and three common modular crypt format hashes. +schemes = + django_pbkdf2_sha256, django_pbkdf2_sha1, django_bcrypt, django_bcrypt_sha256, + django_salted_sha1, django_salted_md5, django_des_crypt, hex_md5, + sha512_crypt, bcrypt, phpass + +; default scheme to use for new hashes +default = django_pbkdf2_sha256 + +; hashes using these schemes will automatically be re-hashed +; when the user logs in (currently all django 1.0 hashes) +deprecated = + django_pbkdf2_sha1, django_salted_sha1, django_salted_md5, + django_des_crypt, hex_md5 + +; sets some common options, including minimum rounds for two primary hashes. +; if a hash has less than this number of rounds, it will be re-hashed. +sha512_crypt__min_rounds = 80000 +django_pbkdf2_sha256__min_rounds = 10000 + +; set somewhat stronger iteration counts for ``User.is_staff`` +staff__sha512_crypt__default_rounds = 100000 +staff__django_pbkdf2_sha256__default_rounds = 12500 + +; and even stronger ones for ``User.is_superuser`` +superuser__sha512_crypt__default_rounds = 120000 +superuser__django_pbkdf2_sha256__default_rounds = 15000 +""" + +#============================================================================= +# helpers +#============================================================================= + +#: prefix used to shoehorn passlib's handler names into django hasher namespace +PASSLIB_WRAPPER_PREFIX = "passlib_" + +#: prefix used by all the django-specific hash formats in passlib; +#: all of these hashes should have a ``.django_name`` attribute. +DJANGO_COMPAT_PREFIX = "django_" + +#: set of hashes w/o "django_" prefix, but which also expose ``.django_name``. +_other_django_hashes = set(["hex_md5"]) + +def _wrap_method(method): + """wrap method object in bare function""" + @wraps(method) + def wrapper(*args, **kwds): + return method(*args, **kwds) + return wrapper + +#============================================================================= +# translator +#============================================================================= +class DjangoTranslator(object): + """ + Object which helps translate passlib hasher objects / names + to and from django hasher objects / names. + + These methods are wrapped in a class so that results can be cached, + but with the ability to have independant caches, since django hasher + names may / may not correspond to the same instance (or even class). + """ + #============================================================================= + # instance attrs + #============================================================================= + + #: CryptContext instance + #: (if any -- generally only set by DjangoContextAdapter subclass) + context = None + + #: internal cache of passlib hasher -> django hasher instance. + #: key stores weakref to passlib hasher. + _django_hasher_cache = None + + #: special case -- unsalted_sha1 + _django_unsalted_sha1 = None + + #: internal cache of django name -> passlib hasher + #: value stores weakrefs to passlib hasher. + _passlib_hasher_cache = None + + #============================================================================= + # init + #============================================================================= + + def __init__(self, context=None, **kwds): + super(DjangoTranslator, self).__init__(**kwds) + if context is not None: + self.context = context + + self._django_hasher_cache = weakref.WeakKeyDictionary() + self._passlib_hasher_cache = weakref.WeakValueDictionary() + + def reset_hashers(self): + self._django_hasher_cache.clear() + self._passlib_hasher_cache.clear() + self._django_unsalted_sha1 = None + + def _get_passlib_hasher(self, passlib_name): + """ + resolve passlib hasher by name, using context if available. + """ + context = self.context + if context is None: + return registry.get_crypt_handler(passlib_name) + else: + return context.handler(passlib_name) + + #============================================================================= + # resolve passlib hasher -> django hasher + #============================================================================= + + def passlib_to_django_name(self, passlib_name): + """ + Convert passlib hasher / name to Django hasher name. + """ + return self.passlib_to_django(passlib_name).algorithm + + # XXX: add option (in class, or call signature) to always return a wrapper, + # rather than native builtin -- would let HashersTest check that + # our own wrapper + implementations are matching up with their tests. + def passlib_to_django(self, passlib_hasher, cached=True): + """ + Convert passlib hasher / name to Django hasher. + + :param passlib_hasher: + passlib hasher / name + + :returns: + django hasher instance + """ + # resolve names to hasher + if not hasattr(passlib_hasher, "name"): + passlib_hasher = self._get_passlib_hasher(passlib_hasher) + + # check cache + if cached: + cache = self._django_hasher_cache + try: + return cache[passlib_hasher] + except KeyError: + pass + result = cache[passlib_hasher] = \ + self.passlib_to_django(passlib_hasher, cached=False) + return result + + # find native equivalent, and return wrapper if there isn't one + django_name = getattr(passlib_hasher, "django_name", None) + if django_name: + return self._create_django_hasher(django_name) + else: + return _PasslibHasherWrapper(passlib_hasher) + + _builtin_django_hashers = dict( + md5="MD5PasswordHasher", + ) + + if DJANGO_VERSION > (2, 1): + # present but disabled by default as of django 2.1; not sure when added, + # so not listing it by default. + _builtin_django_hashers.update( + bcrypt="BCryptPasswordHasher", + ) + + def _create_django_hasher(self, django_name): + """ + helper to create new django hasher by name. + wraps underlying django methods. + """ + # if we haven't patched django, can use it directly + module = sys.modules.get("passlib.ext.django.models") + if module is None or not module.adapter.patched: + from django.contrib.auth.hashers import get_hasher + try: + return get_hasher(django_name) + except ValueError as err: + if not str(err).startswith("Unknown password hashing algorithm"): + raise + else: + # We've patched django's get_hashers(), so calling django's get_hasher() + # or get_hashers_by_algorithm() would only land us back here. + # As non-ideal workaround, have to use original get_hashers(), + get_hashers = module.adapter._manager.getorig("django.contrib.auth.hashers:get_hashers").__wrapped__ + for hasher in get_hashers(): + if hasher.algorithm == django_name: + return hasher + + # hardcode a few for cases where get_hashers() lookup won't work + # (mainly, hashers that are present in django, but disabled by their default config) + path = self._builtin_django_hashers.get(django_name) + if path: + if "." not in path: + path = "django.contrib.auth.hashers." + path + from django.utils.module_loading import import_string + return import_string(path)() + + raise ValueError("unknown hasher: %r" % django_name) + + #============================================================================= + # reverse django -> passlib + #============================================================================= + + def django_to_passlib_name(self, django_name): + """ + Convert Django hasher / name to Passlib hasher name. + """ + return self.django_to_passlib(django_name).name + + def django_to_passlib(self, django_name, cached=True): + """ + Convert Django hasher / name to Passlib hasher / name. + If present, CryptContext will be checked instead of main registry. + + :param django_name: + Django hasher class or algorithm name. + "default" allowed if context provided. + + :raises ValueError: + if can't resolve hasher. + + :returns: + passlib hasher or name + """ + # check for django hasher + if hasattr(django_name, "algorithm"): + + # check for passlib adapter + if isinstance(django_name, _PasslibHasherWrapper): + return django_name.passlib_handler + + # resolve django hasher -> name + django_name = django_name.algorithm + + # check cache + if cached: + cache = self._passlib_hasher_cache + try: + return cache[django_name] + except KeyError: + pass + result = cache[django_name] = \ + self.django_to_passlib(django_name, cached=False) + return result + + # check if it's an obviously-wrapped name + if django_name.startswith(PASSLIB_WRAPPER_PREFIX): + passlib_name = django_name[len(PASSLIB_WRAPPER_PREFIX):] + return self._get_passlib_hasher(passlib_name) + + # resolve default + if django_name == "default": + context = self.context + if context is None: + raise TypeError("can't determine default scheme w/ context") + return context.handler() + + # special case: Django uses a separate hasher for "sha1$$digest" + # hashes (unsalted_sha1) and "sha1$salt$digest" (sha1); + # but passlib uses "django_salted_sha1" for both of these. + if django_name == "unsalted_sha1": + django_name = "sha1" + + # resolve name + # XXX: bother caching these lists / mapping? + # not needed in long-term due to cache above. + context = self.context + if context is None: + # check registry + # TODO: should make iteration via registry easier + candidates = ( + registry.get_crypt_handler(passlib_name) + for passlib_name in registry.list_crypt_handlers() + if passlib_name.startswith(DJANGO_COMPAT_PREFIX) or + passlib_name in _other_django_hashes + ) + else: + # check context + candidates = context.schemes(resolve=True) + for handler in candidates: + if getattr(handler, "django_name", None) == django_name: + return handler + + # give up + # NOTE: this should only happen for custom django hashers that we don't + # know the equivalents for. _HasherHandler (below) is work in + # progress that would allow us to at least return a wrapper. + raise ValueError("can't translate django name to passlib name: %r" % + (django_name,)) + + #============================================================================= + # django hasher lookup + #============================================================================= + + def resolve_django_hasher(self, django_name, cached=True): + """ + Take in a django algorithm name, return django hasher. + """ + # check for django hasher + if hasattr(django_name, "algorithm"): + return django_name + + # resolve to passlib hasher + passlib_hasher = self.django_to_passlib(django_name, cached=cached) + + # special case: Django uses a separate hasher for "sha1$$digest" + # hashes (unsalted_sha1) and "sha1$salt$digest" (sha1); + # but passlib uses "django_salted_sha1" for both of these. + # XXX: this isn't ideal way to handle this. would like to do something + # like pass "django_variant=django_name" into passlib_to_django(), + # and have it cache separate hasher there. + # but that creates a LOT of complication in it's cache structure, + # for what is just one special case. + if django_name == "unsalted_sha1" and passlib_hasher.name == "django_salted_sha1": + if not cached: + return self._create_django_hasher(django_name) + result = self._django_unsalted_sha1 + if result is None: + result = self._django_unsalted_sha1 = self._create_django_hasher(django_name) + return result + + # lookup corresponding django hasher + return self.passlib_to_django(passlib_hasher, cached=cached) + + #============================================================================= + # eoc + #============================================================================= + +#============================================================================= +# adapter +#============================================================================= +class DjangoContextAdapter(DjangoTranslator): + """ + Object which tries to adapt a Passlib CryptContext object, + using a Django-hasher compatible API. + + When installed in django, :mod:`!passlib.ext.django` will create + an instance of this class, and then monkeypatch the appropriate + methods into :mod:`!django.contrib.auth` and other appropriate places. + """ + #============================================================================= + # instance attrs + #============================================================================= + + #: CryptContext instance we're wrapping + context = None + + #: ref to original make_password(), + #: needed to generate usuable passwords that match django + _orig_make_password = None + + #: ref to django helper of this name -- not monkeypatched + is_password_usable = None + + #: PatchManager instance used to track installation + _manager = None + + #: whether config=disabled flag was set + enabled = True + + #: patch status + patched = False + + #============================================================================= + # init + #============================================================================= + def __init__(self, context=None, get_user_category=None, **kwds): + + # init log + self.log = logging.getLogger(__name__ + ".DjangoContextAdapter") + + # init parent, filling in default context object + if context is None: + context = CryptContext() + super(DjangoContextAdapter, self).__init__(context=context, **kwds) + + # setup user category + if get_user_category: + assert callable(get_user_category) + self.get_user_category = get_user_category + + # install lru cache wrappers + try: + from functools import lru_cache # new py32 + except ImportError: + from django.utils.lru_cache import lru_cache # py2 compat, removed in django 3 (or earlier?) + self.get_hashers = lru_cache()(self.get_hashers) + + # get copy of original make_password + from django.contrib.auth.hashers import make_password + if make_password.__module__.startswith("passlib."): + make_password = _PatchManager.peek_unpatched_func(make_password) + self._orig_make_password = make_password + + # get other django helpers + from django.contrib.auth.hashers import is_password_usable + self.is_password_usable = is_password_usable + + # init manager + mlog = logging.getLogger(__name__ + ".DjangoContextAdapter._manager") + self._manager = _PatchManager(log=mlog) + + def reset_hashers(self): + """ + Wrapper to manually reset django's hasher lookup cache + """ + # resets cache for .get_hashers() & .get_hashers_by_algorithm() + from django.contrib.auth.hashers import reset_hashers + reset_hashers(setting="PASSWORD_HASHERS") + + # reset internal caches + super(DjangoContextAdapter, self).reset_hashers() + + #============================================================================= + # django hashers helpers -- hasher lookup + #============================================================================= + + # lru_cache()'ed by init + def get_hashers(self): + """ + Passlib replacement for get_hashers() -- + Return list of available django hasher classes + """ + passlib_to_django = self.passlib_to_django + return [passlib_to_django(hasher) + for hasher in self.context.schemes(resolve=True)] + + def get_hasher(self, algorithm="default"): + """ + Passlib replacement for get_hasher() -- + Return django hasher by name + """ + return self.resolve_django_hasher(algorithm) + + def identify_hasher(self, encoded): + """ + Passlib replacement for identify_hasher() -- + Identify django hasher based on hash. + """ + handler = self.context.identify(encoded, resolve=True, required=True) + if handler.name == "django_salted_sha1" and encoded.startswith("sha1$$"): + # Django uses a separate hasher for "sha1$$digest" hashes, but + # passlib identifies it as belonging to "sha1$salt$digest" handler. + # We want to resolve to correct django hasher. + return self.get_hasher("unsalted_sha1") + return self.passlib_to_django(handler) + + #============================================================================= + # django.contrib.auth.hashers helpers -- password helpers + #============================================================================= + + def make_password(self, password, salt=None, hasher="default"): + """ + Passlib replacement for make_password() + """ + if password is None: + return self._orig_make_password(None) + # NOTE: relying on hasher coming from context, and thus having + # context-specific config baked into it. + passlib_hasher = self.django_to_passlib(hasher) + if "salt" not in passlib_hasher.setting_kwds: + # ignore salt param even if preset + pass + elif hasher.startswith("unsalted_"): + # Django uses a separate 'unsalted_sha1' hasher for "sha1$$digest", + # but passlib just reuses it's "sha1" handler ("sha1$salt$digest"). To make + # this work, have to explicitly tell the sha1 handler to use an empty salt. + passlib_hasher = passlib_hasher.using(salt="") + elif salt: + # Django make_password() autogenerates a salt if salt is bool False (None / ''), + # so we only pass the keyword on if there's actually a fixed salt. + passlib_hasher = passlib_hasher.using(salt=salt) + return passlib_hasher.hash(password) + + def check_password(self, password, encoded, setter=None, preferred="default"): + """ + Passlib replacement for check_password() + """ + # XXX: this currently ignores "preferred" keyword, since its purpose + # was for hash migration, and that's handled by the context. + # XXX: honor "none_causes_check_password_error" quirk for django 2.2+? + # seems safer to return False. + if password is None or not self.is_password_usable(encoded): + return False + + # verify password + context = self.context + try: + correct = context.verify(password, encoded) + except exc.UnknownHashError: + # As of django 1.5, unidentifiable hashes returns False + # (side-effect of django issue 18453) + return False + + if not (correct and setter): + return correct + + # check if we need to rehash + if preferred == "default": + if not context.needs_update(encoded, secret=password): + return correct + else: + # Django's check_password() won't call setter() on a + # 'preferred' alg, even if it's otherwise deprecated. To try and + # replicate this behavior if preferred is set, we look up the + # passlib hasher, and call it's original needs_update() method. + # TODO: Solve redundancy that verify() call + # above is already identifying hash. + hasher = self.django_to_passlib(preferred) + if (hasher.identify(encoded) and + not hasher.needs_update(encoded, secret=password)): + # alg is 'preferred' and hash itself doesn't need updating, + # so nothing to do. + return correct + # else: either hash isn't preferred, or it needs updating. + + # call setter to rehash + setter(password) + return correct + + #============================================================================= + # django users helpers + #============================================================================= + + def user_check_password(self, user, password): + """ + Passlib replacement for User.check_password() + """ + if password is None: + return False + hash = user.password + if not self.is_password_usable(hash): + return False + cat = self.get_user_category(user) + try: + ok, new_hash = self.context.verify_and_update(password, hash, category=cat) + except exc.UnknownHashError: + # As of django 1.5, unidentifiable hashes returns False + # (side-effect of django issue 18453) + return False + if ok and new_hash is not None: + # migrate to new hash if needed. + user.password = new_hash + user.save() + return ok + + def user_set_password(self, user, password): + """ + Passlib replacement for User.set_password() + """ + if password is None: + user.set_unusable_password() + else: + cat = self.get_user_category(user) + user.password = self.context.hash(password, category=cat) + + def get_user_category(self, user): + """ + Helper for hashing passwords per-user -- + figure out the CryptContext category for specified Django user object. + .. note:: + This may be overridden via PASSLIB_GET_CATEGORY django setting + """ + if user.is_superuser: + return "superuser" + elif user.is_staff: + return "staff" + else: + return None + + #============================================================================= + # patch control + #============================================================================= + + HASHERS_PATH = "django.contrib.auth.hashers" + MODELS_PATH = "django.contrib.auth.models" + USER_CLASS_PATH = MODELS_PATH + ":User" + FORMS_PATH = "django.contrib.auth.forms" + + #: list of locations to patch + patch_locations = [ + # + # User object + # NOTE: could leave defaults alone, but want to have user available + # so that we can support get_user_category() + # + (USER_CLASS_PATH + ".check_password", "user_check_password", dict(method=True)), + (USER_CLASS_PATH + ".set_password", "user_set_password", dict(method=True)), + + # + # Hashers module + # + (HASHERS_PATH + ":", "check_password"), + (HASHERS_PATH + ":", "make_password"), + (HASHERS_PATH + ":", "get_hashers"), + (HASHERS_PATH + ":", "get_hasher"), + (HASHERS_PATH + ":", "identify_hasher"), + + # + # Patch known imports from hashers module + # + (MODELS_PATH + ":", "check_password"), + (MODELS_PATH + ":", "make_password"), + (FORMS_PATH + ":", "get_hasher"), + (FORMS_PATH + ":", "identify_hasher"), + + ] + + def install_patch(self): + """ + Install monkeypatch to replace django hasher framework. + """ + # don't reapply + log = self.log + if self.patched: + log.warning("monkeypatching already applied, refusing to reapply") + return False + + # version check + if DJANGO_VERSION < MIN_DJANGO_VERSION: + raise RuntimeError("passlib.ext.django requires django >= %s" % + (MIN_DJANGO_VERSION,)) + + # log start + log.debug("preparing to monkeypatch django ...") + + # run through patch locations + manager = self._manager + for record in self.patch_locations: + if len(record) == 2: + record += ({},) + target, source, opts = record + if target.endswith((":", ",")): + target += source + value = getattr(self, source) + if opts.get("method"): + # have to wrap our method in a function, + # since we're installing it in a class *as* a method + # XXX: make this a flag for .patch()? + value = _wrap_method(value) + manager.patch(target, value) + + # reset django's caches (e.g. get_hash_by_algorithm) + self.reset_hashers() + + # done! + self.patched = True + log.debug("... finished monkeypatching django") + return True + + def remove_patch(self): + """ + Remove monkeypatch from django hasher framework. + As precaution in case there are lingering refs to context, + context object will be wiped. + + .. warning:: + This may cause problems if any other Django modules have imported + their own copies of the patched functions, though the patched + code has been designed to throw an error as soon as possible in + this case. + """ + log = self.log + manager = self._manager + + if self.patched: + log.debug("removing django monkeypatching...") + manager.unpatch_all(unpatch_conflicts=True) + self.context.load({}) + self.patched = False + self.reset_hashers() + log.debug("...finished removing django monkeypatching") + return True + + if manager.isactive(): # pragma: no cover -- sanity check + log.warning("reverting partial monkeypatching of django...") + manager.unpatch_all() + self.context.load({}) + self.reset_hashers() + log.debug("...finished removing django monkeypatching") + return True + + log.debug("django not monkeypatched") + return False + + #============================================================================= + # loading config + #============================================================================= + + def load_model(self): + """ + Load configuration from django, and install patch. + """ + self._load_settings() + if self.enabled: + try: + self.install_patch() + except: + # try to undo what we can + self.remove_patch() + raise + else: + if self.patched: # pragma: no cover -- sanity check + log.error("didn't expect monkeypatching would be applied!") + self.remove_patch() + log.debug("passlib.ext.django loaded") + + def _load_settings(self): + """ + Update settings from django + """ + from django.conf import settings + + # TODO: would like to add support for inheriting config from a preset + # (or from existing hasher state) and letting PASSLIB_CONFIG + # be an update, not a replacement. + + # TODO: wrap and import any custom hashers as passlib handlers, + # so they could be used in the passlib config. + + # load config from settings + _UNSET = object() + config = getattr(settings, "PASSLIB_CONFIG", _UNSET) + if config is _UNSET: + # XXX: should probably deprecate this alias + config = getattr(settings, "PASSLIB_CONTEXT", _UNSET) + if config is _UNSET: + config = "passlib-default" + if config is None: + warn("setting PASSLIB_CONFIG=None is deprecated, " + "and support will be removed in Passlib 1.8, " + "use PASSLIB_CONFIG='disabled' instead.", + DeprecationWarning) + config = "disabled" + elif not isinstance(config, (unicode, bytes, dict)): + raise exc.ExpectedTypeError(config, "str or dict", "PASSLIB_CONFIG") + + # load custom category func (if any) + get_category = getattr(settings, "PASSLIB_GET_CATEGORY", None) + if get_category and not callable(get_category): + raise exc.ExpectedTypeError(get_category, "callable", "PASSLIB_GET_CATEGORY") + + # check if we've been disabled + if config == "disabled": + self.enabled = False + return + else: + self.__dict__.pop("enabled", None) + + # resolve any preset aliases + if isinstance(config, str) and '\n' not in config: + config = get_preset_config(config) + + # setup category func + if get_category: + self.get_user_category = get_category + else: + self.__dict__.pop("get_category", None) + + # setup context + self.context.load(config) + self.reset_hashers() + + #============================================================================= + # eof + #============================================================================= + +#============================================================================= +# wrapping passlib handlers as django hashers +#============================================================================= +_GEN_SALT_SIGNAL = "--!!!generate-new-salt!!!--" + +class ProxyProperty(object): + """helper that proxies another attribute""" + + def __init__(self, attr): + self.attr = attr + + def __get__(self, obj, cls): + if obj is None: + cls = obj + return getattr(obj, self.attr) + + def __set__(self, obj, value): + setattr(obj, self.attr, value) + + def __delete__(self, obj): + delattr(obj, self.attr) + + +class _PasslibHasherWrapper(object): + """ + adapter which which wraps a :cls:`passlib.ifc.PasswordHash` class, + and provides an interface compatible with the Django hasher API. + + :param passlib_handler: + passlib hash handler (e.g. :cls:`passlib.hash.sha256_crypt`. + """ + #===================================================================== + # instance attrs + #===================================================================== + + #: passlib handler that we're adapting. + passlib_handler = None + + # NOTE: 'rounds' attr will store variable rounds, IF handler supports it. + # 'iterations' will act as proxy, for compatibility with django pbkdf2 hashers. + # rounds = None + # iterations = None + + #===================================================================== + # init + #===================================================================== + def __init__(self, passlib_handler): + # init handler + if getattr(passlib_handler, "django_name", None): + raise ValueError("handlers that reflect an official django " + "hasher shouldn't be wrapped: %r" % + (passlib_handler.name,)) + if passlib_handler.is_disabled: + # XXX: could this be implemented? + raise ValueError("can't wrap disabled-hash handlers: %r" % + (passlib_handler.name)) + self.passlib_handler = passlib_handler + + # init rounds support + if self._has_rounds: + self.rounds = passlib_handler.default_rounds + self.iterations = ProxyProperty("rounds") + + #===================================================================== + # internal methods + #===================================================================== + def __repr__(self): + return "" % self.passlib_handler + + #===================================================================== + # internal properties + #===================================================================== + + @memoized_property + def __name__(self): + return "Passlib_%s_PasswordHasher" % self.passlib_handler.name.title() + + @memoized_property + def _has_rounds(self): + return "rounds" in self.passlib_handler.setting_kwds + + @memoized_property + def _translate_kwds(self): + """ + internal helper for safe_summary() -- + used to translate passlib hash options -> django keywords + """ + out = dict(checksum="hash") + if self._has_rounds and "pbkdf2" in self.passlib_handler.name: + out['rounds'] = 'iterations' + return out + + #===================================================================== + # hasher properties + #===================================================================== + + @memoized_property + def algorithm(self): + return PASSLIB_WRAPPER_PREFIX + self.passlib_handler.name + + #===================================================================== + # hasher api + #===================================================================== + def salt(self): + # NOTE: passlib's handler.hash() should generate new salt each time, + # so this just returns a special constant which tells + # encode() (below) not to pass a salt keyword along. + return _GEN_SALT_SIGNAL + + def verify(self, password, encoded): + return self.passlib_handler.verify(password, encoded) + + def encode(self, password, salt=None, rounds=None, iterations=None): + kwds = {} + if salt is not None and salt != _GEN_SALT_SIGNAL: + kwds['salt'] = salt + if self._has_rounds: + if rounds is not None: + kwds['rounds'] = rounds + elif iterations is not None: + kwds['rounds'] = iterations + else: + kwds['rounds'] = self.rounds + elif rounds is not None or iterations is not None: + warn("%s.hash(): 'rounds' and 'iterations' are ignored" % self.__name__) + handler = self.passlib_handler + if kwds: + handler = handler.using(**kwds) + return handler.hash(password) + + def safe_summary(self, encoded): + from django.contrib.auth.hashers import mask_hash + from django.utils.translation import ugettext_noop as _ + handler = self.passlib_handler + items = [ + # since this is user-facing, we're reporting passlib's name, + # without the distracting PASSLIB_HASHER_PREFIX prepended. + (_('algorithm'), handler.name), + ] + if hasattr(handler, "parsehash"): + kwds = handler.parsehash(encoded, sanitize=mask_hash) + for key, value in iteritems(kwds): + key = self._translate_kwds.get(key, key) + items.append((_(key), value)) + return OrderedDict(items) + + def must_update(self, encoded): + # TODO: would like access CryptContext, would need caller to pass it to get_passlib_hasher(). + # for now (as of passlib 1.6.6), replicating django policy that this returns True + # if 'encoded' hash has different rounds value from self.rounds + if self._has_rounds: + # XXX: could cache this subclass somehow (would have to intercept writes to self.rounds) + # TODO: always call subcls/handler.needs_update() in case there's other things to check + subcls = self.passlib_handler.using(min_rounds=self.rounds, max_rounds=self.rounds) + if subcls.needs_update(encoded): + return True + return False + + #===================================================================== + # eoc + #===================================================================== + +#============================================================================= +# adapting django hashers -> passlib handlers +#============================================================================= +# TODO: this code probably halfway works, mainly just needs +# a routine to read HASHERS and PREFERRED_HASHER. + +##from passlib.registry import register_crypt_handler +##from passlib.utils import classproperty, to_native_str, to_unicode +##from passlib.utils.compat import unicode +## +## +##class _HasherHandler(object): +## "helper for wrapping Hasher instances as passlib handlers" +## # FIXME: this generic wrapper doesn't handle custom settings +## # FIXME: genconfig / genhash not supported. +## +## def __init__(self, hasher): +## self.django_hasher = hasher +## if hasattr(hasher, "iterations"): +## # assume encode() accepts an "iterations" parameter. +## # fake min/max rounds +## self.min_rounds = 1 +## self.max_rounds = 0xFFFFffff +## self.default_rounds = self.django_hasher.iterations +## self.setting_kwds += ("rounds",) +## +## # hasher instance - filled in by constructor +## django_hasher = None +## +## setting_kwds = ("salt",) +## context_kwds = () +## +## @property +## def name(self): +## # XXX: need to make sure this wont' collide w/ builtin django hashes. +## # maybe by renaming this to django compatible aliases? +## return DJANGO_PASSLIB_PREFIX + self.django_name +## +## @property +## def django_name(self): +## # expose this so hasher_to_passlib_name() extracts original name +## return self.django_hasher.algorithm +## +## @property +## def ident(self): +## # this should always be correct, as django relies on ident prefix. +## return unicode(self.django_name + "$") +## +## @property +## def identify(self, hash): +## # this should always work, as django relies on ident prefix. +## return to_unicode(hash, "latin-1", "hash").startswith(self.ident) +## +## @property +## def hash(self, secret, salt=None, **kwds): +## # NOTE: from how make_password() is coded, all hashers +## # should have salt param. but only some will have +## # 'iterations' parameter. +## opts = {} +## if 'rounds' in self.setting_kwds and 'rounds' in kwds: +## opts['iterations'] = kwds.pop("rounds") +## if kwds: +## raise TypeError("unexpected keyword arguments: %r" % list(kwds)) +## if isinstance(secret, unicode): +## secret = secret.encode("utf-8") +## if salt is None: +## salt = self.django_hasher.salt() +## return to_native_str(self.django_hasher(secret, salt, **opts)) +## +## @property +## def verify(self, secret, hash): +## hash = to_native_str(hash, "utf-8", "hash") +## if isinstance(secret, unicode): +## secret = secret.encode("utf-8") +## return self.django_hasher.verify(secret, hash) +## +##def register_hasher(hasher): +## handler = _HasherHandler(hasher) +## register_crypt_handler(handler) +## return handler + +#============================================================================= +# monkeypatch helpers +#============================================================================= +# private singleton indicating lack-of-value +_UNSET = object() + +class _PatchManager(object): + """helper to manage monkeypatches and run sanity checks""" + + # NOTE: this could easily use a dict interface, + # but keeping it distinct to make clear that it's not a dict, + # since it has important side-effects. + + #=================================================================== + # init and support + #=================================================================== + def __init__(self, log=None): + # map of key -> (original value, patched value) + # original value may be _UNSET + self.log = log or logging.getLogger(__name__ + "._PatchManager") + self._state = {} + + def isactive(self): + return bool(self._state) + + # bool value tests if any patches are currently applied. + # NOTE: this behavior is deprecated in favor of .isactive + __bool__ = __nonzero__ = isactive + + def _import_path(self, path): + """retrieve obj and final attribute name from resource path""" + name, attr = path.split(":") + obj = __import__(name, fromlist=[attr], level=0) + while '.' in attr: + head, attr = attr.split(".", 1) + obj = getattr(obj, head) + return obj, attr + + @staticmethod + def _is_same_value(left, right): + """check if two values are the same (stripping method wrappers, etc)""" + return get_method_function(left) == get_method_function(right) + + #=================================================================== + # reading + #=================================================================== + def _get_path(self, key, default=_UNSET): + obj, attr = self._import_path(key) + return getattr(obj, attr, default) + + def get(self, path, default=None): + """return current value for path""" + return self._get_path(path, default) + + def getorig(self, path, default=None): + """return original (unpatched) value for path""" + try: + value, _= self._state[path] + except KeyError: + value = self._get_path(path) + return default if value is _UNSET else value + + def check_all(self, strict=False): + """run sanity check on all keys, issue warning if out of sync""" + same = self._is_same_value + for path, (orig, expected) in iteritems(self._state): + if same(self._get_path(path), expected): + continue + msg = "another library has patched resource: %r" % path + if strict: + raise RuntimeError(msg) + else: + warn(msg, PasslibRuntimeWarning) + + #=================================================================== + # patching + #=================================================================== + def _set_path(self, path, value): + obj, attr = self._import_path(path) + if value is _UNSET: + if hasattr(obj, attr): + delattr(obj, attr) + else: + setattr(obj, attr, value) + + def patch(self, path, value, wrap=False): + """monkeypatch object+attr at to have , stores original""" + assert value != _UNSET + current = self._get_path(path) + try: + orig, expected = self._state[path] + except KeyError: + self.log.debug("patching resource: %r", path) + orig = current + else: + self.log.debug("modifying resource: %r", path) + if not self._is_same_value(current, expected): + warn("overridding resource another library has patched: %r" + % path, PasslibRuntimeWarning) + if wrap: + assert callable(value) + wrapped = orig + wrapped_by = value + def wrapper(*args, **kwds): + return wrapped_by(wrapped, *args, **kwds) + update_wrapper(wrapper, value) + value = wrapper + if callable(value): + # needed by DjangoContextAdapter init + get_method_function(value)._patched_original_value = orig + self._set_path(path, value) + self._state[path] = (orig, value) + + @classmethod + def peek_unpatched_func(cls, value): + return value._patched_original_value + + ##def patch_many(self, **kwds): + ## "override specified resources with new values" + ## for path, value in iteritems(kwds): + ## self.patch(path, value) + + def monkeypatch(self, parent, name=None, enable=True, wrap=False): + """function decorator which patches function of same name in """ + def builder(func): + if enable: + sep = "." if ":" in parent else ":" + path = parent + sep + (name or func.__name__) + self.patch(path, func, wrap=wrap) + return func + if callable(name): + # called in non-decorator mode + func = name + name = None + builder(func) + return None + return builder + + #=================================================================== + # unpatching + #=================================================================== + def unpatch(self, path, unpatch_conflicts=True): + try: + orig, expected = self._state[path] + except KeyError: + return + current = self._get_path(path) + self.log.debug("unpatching resource: %r", path) + if not self._is_same_value(current, expected): + if unpatch_conflicts: + warn("reverting resource another library has patched: %r" + % path, PasslibRuntimeWarning) + else: + warn("not reverting resource another library has patched: %r" + % path, PasslibRuntimeWarning) + del self._state[path] + return + self._set_path(path, orig) + del self._state[path] + + def unpatch_all(self, **kwds): + for key in list(self._state): + self.unpatch(key, **kwds) + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/handlers/__init__.py b/ansible/lib/python3.11/site-packages/passlib/handlers/__init__.py new file mode 100644 index 000000000..0a0338c86 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/handlers/__init__.py @@ -0,0 +1 @@ +"""passlib.handlers -- holds implementations of all passlib's builtin hash formats""" diff --git a/ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..872a257bbf05ba08cbd6fe44d74f097bd2e128a4 GIT binary patch literal 285 zcmXw!y-EZz5XTc072yisAe~&X8xC9X5wtk8unDu5+$NZ0LMB_BFJfma_CAQE*!lv( zbyt~%-9N*8F~e`>eX+P;Rd4-g|5)SiSj^&6vL8oYnPyK+^EF!@z1$}t@~&$wMD0x` z(KN!M*Cbp>-UJg16o-)nZUuNRriCjK-Qaw2QCJ`=wwMDHx?A#}Go>_cl=@)%k$YH6 zi@`_=Fl3Y|qm~fKb>7HImT6Zk#_M*qlH`hM5^SEKHzX&LoKKTVx#)^gZQ32qZvyJU ao7;R^8RLU=S>4|;=lq*Je9e|mYWE+M;8v0V literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/argon2.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/argon2.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2ad8392a914ca9c979fbc9563ea56261b8da7b62 GIT binary patch literal 32317 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 literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/bcrypt.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/bcrypt.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e89de582986872b8fc6ea2fb5b6d29119471744b GIT binary patch literal 43326 zcmd_T3ve4pnkHCy5+Ff>1W52LffOme!G|bHqGUa3JuHcmEm@Y}2Luuz1qpn(08%2F z>fv}j)3nz!O?%}NXU4wf?%E!D?2ggw_FQOo`!?J+eY-uk9TNZ#8^YmM7rNc`#W{9w zk6yWN9er`p_x+hwg{lJRN6*g0L~IdA7BaK4va&M&_y6Y~mX;O^IR4woA5D&?1mWM( z5BXpv`0=>GBnWqfkl+_WhLAC8m@@i}QzpO3K)+2<^OVJJVdv(kb*ji;#Lg{Io8N|V zOVsYSv;W2ZVxtf)zFs2VAnf?SAP67f%MX7k>RF>@Q%=7VZCHZk_*UTS!q<(mqG;t* zmA{I$Z;N`Ss{PedHU65ZT7NCevq$Tu>izXo4gQ9yMt|efHvhJ%?f&gkUcYy$$=@{9 z>~Egh;omXU;%}L1^|!KXilaNH+Wc+oyd>H_)#2}8=MJ2A`a7q({9RMs{_d$Bf6tW9 z@0;rN_p)+Jv~Q|^+-MM{2K>8_Q#xtz590YcLuIjgtm`}@CjM!0*3Et<2?Zw5B+ZTvYt}G_`vha-kw^fyrtTCe_s1{FcR(_X+ zsb!!gVQv`+vh)^97g##h@|Z!{!~rVp8*JHq^r@FmPrQ8g>G?~0rZ4ywo}HW<9iIzd ziil6YJaY6z@XX~4FJJeGG2g8Je021gzGpA@o;*D+ts(Z0RurxO#}i2I3U-bggxkV& z#SS5EWOZ25B8X;#a773iF9 zGod-Gt?8M#cYC z)z9f2qYEd3bJ4ieNxJZC@CMyKek~I7@~(Pgk?HZU>c%K(56)tZ0p>*)I?%JJAYK!` zXr4eBL5;|GZX9o317r)zFU5=N$HH`zAlAfu82!&(-_06`!~r{%H;+{@H;xw=M!f}Z z>{?_39j3mDH2VnRlmz6(9ta%2*kKm8qaa&yia{y{IP(|nW8U2ZAMEz;#W zS*}f&TO2X_T_O7|gWv71)byb$RP6VJO8nI!hrb3rtPPd=>q2Gz`jFG#fNvwd+wk3v zuQy!kZ^HlP@QzUVtHK9(;E&j2Q{rz4Re<(t4Y`o+4DVPLCJZ6>t2SM&He6eocWt{= zzbdbOhg9OpE9ne1uzOU$YWH`AYW&@3xi+umo)9Q8zYk>%q54-1y596I3mB6=&@GMG z(!f!MxsJ_DP0dUPcj8U5C3F<9!8;xW+>s!wLd{^20ujcEaFD1N!jXL8T_os$PyAYV z8g$GUYXFNM5CvJ@`Jl*P&lna*7$}!R0zm5-z(6q)3VUz%dZogi*lc(_G7%Z~a#R_U zV9|5bbWpBEH;c{$Lj(xv4g|7e)HV8{z+V0wShGMB?CAia2q2?*bPo5QC1ef&MOTf_ zh$4Wde1}{=?BT)j}zdL4=YX>vi)tmpu57NuqKyi{x2G| zOW*~xaoc#?v}jZ#)f_3nl&w&wB?J|2}1%;e}nZ!^u}qdq~m&#e72^57deOiFA5>^2YA+xl##?! zSwdhef6G{_R74K))H%0pw~LIpdeORAv}l_JNLw_1Y}K+6SUg3eiG!T#8;j1|oB*W( zcl^>14-64el0wH1Ehf1MMd_kBfv>oKJ9F$MxGqK7#O&mExr_t&V@OK zv&29T#6^1ItO>|7Yn;25HHB}DXI*FIyAtWmYNJilcr=zZ6R*MUk~IT^N3+FzhBLy{ z916yRs&1Lauic2zv>@cm4zZQ)RzlRNM2077HIT-qDq2U~*uO%uBs}ueKJe_^@a$ZR zr##(hPxosj%a-MMrlkDslN%)s$&!YLt_I+*ZM!}f4DP`%D8bLt!AJGme=z*}!|w;z zO@Dvly@^zPPrAM*Wvf{(UKx2m|L%&!1PVL&n+LctTZ==MQ zEb%>bRo>b8z}2RLaR>_3rmolLtkV6hiJ#)AptjQXZC9JK7%VvghF zt`Jw1>6hW6a9t_IxiXP)t}ITR6YOBegqar0ZG&dX>6SL8u5mO`(i)?g*Mfhs4lA1- z>~-f}tGEwMigif9_L*bh=!AF-KO};PzhOIPRY0^Qs+>+#mVzsZZiRy`5}+HsySF+5FJiuzE4&Y)(6yleT8@6t2?%LV8j* zb}!-NYsSv7)#y!_*eDs}3IGAH(19`h)jZ}B_TgGjS;urZ9Et_zW&t(B zm=H&i$yQRj*`B7)?t7u6^B6w&rTi9|;&c ztKe{c|G?`9R?VrB#&k&|7&?1R#_hR#=&eI*gDH1=+TH%ZJ+R>(NVx~o?!lyE@R7TE z`7|GJ^*|{AF@cu#i&wpL^$X&g3?vYV3Pk46jQ*uu5I!`Fc38wK=mVehWq?xCVG%Q) zEeo+4B}_0EjEE3|%|Wu#Vd5i6l#TcT5(xxKvws{pi3aUZY8(A`bWJ}Jn*TPTyecXkcw&N<}2TDNEz`A>DZ!4tWsn1kWoznYhj#$&wJBoq6q}u1%M;S!9vq*aj*O` zywl;EUV>9iiMu4C1tUkoWiJ>iM#{1>E**w&hjT)Z!^FaIuX<_h_b$xLF{#VVAXq_Q zMZ8d1wkEHG_6{nTh};5O6^ZR^$a{s8T%-? zPRTV&7}Jw^ zAmx58?S5|A_OPKP-LUt5O{(E&y5VTbR*Sh-!{(aB-ty2<{?#+BX4%PRT4EpRt{?${ zK!`*lw9qAKvzKJr?P1@f&)YT|nN_km=xFO0FE~l};<3MH_+Ih%O1|eHf`FJF`N%Oh zuNiWL-kcCC2sqUY5Xe8M6Vzkc(38JCsUxYc#5az6!|O&DJayctcgMX?|ZP zaKeu3N=uMLfY^M!WZAG>vh0{Jg-oy7ZkzDSF<}guac-8*Ew9>MFUEN>@~z0XgoQUiLMvJ@V`xOK4SKKSt6Lf_0?ff2U+bt>FZr=SJh3nz|=6JK}?m)XADX7-~ zMEqLu*5%^q*9@UjNut&84aTahz*sfMTXXjhIe9;~m_cs2ruIF;0D#_FrCh2W&$TK! zSN%U!9;(o`aoZ-%5t}YQ{|)dtqRg9b@Z7Ef&pnv;j@)1Ij(oY^Yv&NXqeV;nE6Tso zXjJN++AfWTU6;SrXpr859p7LystSz8;k?oC6dsK@$>pR2?_61*Y$mJ93XalOl(QM6 zu+s$JI;s;HCa@-=MdD?~ncj~?>Lfywg$~}S$gRjUm)M76ZQcM$#RHNoJ-`Y<^N|zm+jd|PVrf*Eo+?@7G ztY{=GwsF!qmN)>*1zZpU5e=m;)ARy!$7W}yL5;J)YgJzDFzK1;Hmzc}w^i&IzW5=` z6Cec+M5ZTZV1H0HIunOtl$)%CL*gMkp?DZcw(^vuYd^~MHe9T1f~q2G356l??y$hj zBRmdmI+tq{N%Jmi2aO+yFU-RB!pd(Sqfr3?zQnn_Yzed5;AM$n9VyPlDqz{Q^VoMF zI)g%ml3%!l znw`n=R{nYDuDUancKbF3D^-#cu$C2-ckOT4lXaIfbxmtS>x1`OHcdiB9c0iIhYb)y zBZV*p>8I6FTWE2!6fHoj8Y86ecS@YX_Z>%SP4DhQ@{<|^QjJiUC=)iD{}WE$Hp1M_ z0On6v!>{~9uXhQVi2J{d`L~u|8990N&8)r&>%~IkzZSxZp16vV3t3(>{{iTjMFDBi zyE-*3(GRQ*leUdmm0G)m77U<$1f=$NbsC^o454B`x{}~FfIbB}5QNAHZx}($WUP!! zmGv#&y^zz8%Ej9bHWTXr5O4q%Ph%bXH4TO50mz`ApPRhqWhMlqkKwBd;0fF)^nWqv z?dbX-gh9|BN@{!3=t#=S9ORw!T^I=H^@34;&Z~1I+R+cg8=SLh?#@LzspKV5;LRWnK{@oMd!n^ zQj}kTUIF&v3ethH<$pl36C0n8-3*UuO~uA1BfU~Sou0cHg{^{Qv@i*ihVfpla;sN@ zPQGy;6sHr1#QrDxef#!{1Nhh9cTh9nVt4{#1+ord930mO1z8{w^0o}zYT?!5 zL1=|}$Ae=~347ePdof2KAAxlr&Y7WK5c3_THgY$9Stp-)C!pFxE%bzgU~i(BFDiPpQAj|o+>lQs z{Xo|74CFVp4@T2e2K-gP=V37( zAS1+BfEIE9Z6rp%B?>6m%27fF{{lB8WY8ygnwF1esvGVuzO}e^B30d;uI^6uUQbnD zUp|?s_x|9(?;lt%O4awJ>-&BU*V+P;>;1N$J zgtyNat{9R}U#5>gz(1Q7V^z@bxye*L28=){WV#hd%Y@3hm7&$0Yuzb#Z`$2U>dG<0 zLr=5RfU1q;j;kq8FzpE@%Y(4VK{Z~Pi+~iK2oFZ1$*@HhCmdUZpb*h$vA4z^ItU!H zaO|-KHUR7=PJ0b^%0HlCN54K&`W7nEV3vHWpu#NQ2rC@aV}%?p7>i+kg974&qaF{8 zb!cEm0e~I!l0hnHRW;BC#1~1u2S`nelPThJ7Hc$v86_;66t)BK16^R1?(W&sGtdv> zHHL)>TUzuJMk!-+ac>NUW1yX9KwB}HDb{K%J_p-dwm2hdD?4g`Dtx8m^+4-KBR62YL(pXQX1m7Bww+U% z&kwh^_jmF04sWX$_Qf3p_4;4z==Saz=xEct`53HEg}(lQU4y%a_UzsFU+>xIXTQP% zR$>mTw>Us|{}FdaD=JMq-D;KI3+XU;}0M^6pz zp4rSl=3*ouwGvR@O-9V<|%yU<6#`m3zhy)-KL}LsSqlGiHIKhUt`e!xtEgsb2XggpB)n^&t%#&nJk}tdc{DtAUz5_kYgmV zobg)JZ;9w^qCYzf>>WXU*M(ki6#sW{#?^O)@w@WM{z~{=ZsmLQ;)Zccyts_JdF9aB z?v%SD?d~9SS)D2OtXP4p-P@CwUX(t8P!gZ*;goA4?V3nBCfL|AZ7gi&x3q_UX5#GG zbC*x|-8%nVaQ^I`*>j2OPeyhp!n-f_-`LxG0j5zPxD)+3E}QvO#6Z)w$W~t6-taW2 zJ%VdFAxFa7N_NaSa###s=j=OONy~xNV6>6k5F2~4-k0EOqG{_kq)J)aOpLoTB=)QN zNbi+3l#f6Q_xAp`BY0J}DoF4ax-B8qUxSo|IwF5`*+@ZV(%}<(X@>S8NerlZ#mBW} zVBu@t(pQWyE~VEblKQz|+hf0e+qjh4?x^SH>MpJLBQO=5%~mssnPid|nU2IG%(fxK zXl?NTJ#|ig<31aS!J=hSBfhaq2JbT>(fE_F(a+jRW6Xp*;w)OoR!GG8Kwm)eNfJeB zIYtSA>8#`8lNT-qP9C{%GB9vNe2(&&CX7@jSr4%Qfq`HkN23wcCY>K+PIB!p$n7gO zHCJ+lV|j%~v(CT-go?zO2bfr_qcm%0GEP|1#27@%T0`Oj?3Be@)Tt#(ev3NgpcTt_ zlfXb9%yt=DuE|}{{2CJpw2AYSFg}LNjl5SG<$~n!;ls>U2QQ?suqx5|jc}BQAld|I zW5|mL8&wMBweKEJH|)MQnQl0kbV{F$+m9V7Bw9qAp$0 zv~2yfYWw@H_dMyQ{mIG$%l3yQ@E9=I_hcMZ?{=i?I@5K7$=acmV`%x*rn%A%S=U!4 z-R*YbM7}b;XsETH`q`e8<3ieTA;~_OifVe$vxbMRZOQFNJ~@%vemdnk zlXjg+I?lkR$<;_rzh9{7PYYbZnseFrveu4%@?2{BnUw2n+I2SRIQz)alyo$`-=1=G zr5#;KM_0zVZTUCey~WL?Hifd%y%~4i>h%qGd(zzwbW~My_taablG~n7z8FeXhSQbd zB>PYiM$S|>jQ9D(h~e`hQ{4zLn{^HEc+#~)unMT!3#)*ty|4;EQY;MaU9qRj`(W}_ z)AFFYYooeredJzCs(Melde65Uzo&k7BIUT4c3e!d4|`y%y}Qr@gTg6o);+R*3`m~( z*U=?Q;VymJ-bd4+HKXrGUIvD?r+?X=wspWtV>!+#D@E0UB~6qSv8IoSvid5!wXJMQ zVL^fGa~`n}X&Mu+D2#}t4Ut5eYHLyAq7>%}Zvc|*aTpPDGw=3}oYaw|V?;DSfJl<| zZm@F9dCe;^McrIO&;ik9M9eW-FtRYg=m(;l3U0j+ioh)?9tet)F>ZkB0QUt?B<^a~nqUC|Jbpys&6YHUmuip=) z51vapTT{;SY3KQ*?L2=mWZ)tnim{6i!%HuQj4JqX+pLyT!%$UqomLtxdbTn@%{Nq? z?pid99d>AP)>Q4u_k=86XHE@Wy9*AFij{yMKx0Ji@W$KbE5fu{*7T?iRdb#TdP_Q( zaKYdU(iYNg%xZdaR!u?WAE zXTNQn{w-XyQ!a(S2wU7DU8Csa^RBspYdZ6;8N@Y;W?k%FG>ALc6A~>c9e5Uuk%cyN!Z)VgRSLv}6uhj$2#&V&&FsA%+;7WgXn z>^&~Ng3H7qBnh7)a0fWq!ZC%k%M#6>sLSz$9Kyhj3%(`8q(p4U*1{C;<+%vJ0&*jv z0CSCIHq+t-x+P5@&euPSv#je`?ovI%%&9r8NrYnL3O0NHrqxo~4@UTI>070%ld1BZ>2l~f>GS17H>#LQ`&Efm=uW*p83j-4Bhoon+chcE5$B^^F8 zL3gcON|w~)BRSM`Zn!$vccomtX;*L3(FJCLp&Sh22HAJuMOGp)JSOz+gMSg98kvOCZtNBN^t*W1@N zO1C9Tx4|g~P7ZLpDeVTXsclHtbgfvvEKtFO3RX>p$&J#+WND+^f&&$7klaJ`U$H+^ z4XM4gYCnjw=D~Z%?;lMy9ZEMHTD7iPKW*}@&)*YMO~dJ?;Z5;orYAnJq?(>gxsIn@$CHlZ&^fHkE#FSs8e#tI|Mfq=Pi zjC-1u@+<%aQb%S>;l0isBx6ij8Z49VDfv|v)8)!Ju9eP9Bo>&Q z=CY-V*Cl(j*Qlce4*2S(1+M}EAJA<|m{|#TFclROfAJhu^;2?{l5t8VD7k?IkZBqL zCAd}?yy+y7Q-BZRyHw*JQnE(XN+q|B@o+R62z+SZHlSG50%sYSojglkLSBsY9N_%q zzfE(I#fHg;WSiiqTpG!^4=)i5Q&qimHdED@u4-KxrJs%XSy{h&dF|G{n)`c`?vbT4 znJUl9(<`wzFWtGc`rKM@Ex2y}`;+fYuHU-fgujtLYx{BACwqCN_?yq&d2aO@lucFb zOJ_G7LTg*nymRU7%GpdsEm+B=r?5U?laa(hMtTVKpt58bAU|jZ#J#Wvs|-0n@BWluDG#yXYW4drhKL zqTB{MwZBy|C~Sj@&|LS(0=wH|%w?Zar3IFC$!f7Yji*tiynoLbEN}_QrOIWwEOjwI zXS6e$P&8qcL*{ZGHzP~$rO$lHjj!7LX66%4v209$ETc1$tSGU>I2dkR20nol&0t>0 zejh$Fd<~GW3fYRS7-w1FyGD^8Vx;OCBaiSP1-=l+!_1L69o_FX;-wT2`hDse~ zzyf+anFd1K7d|B1gJC=BWXrT19i-+8f5jmJ-O=k^;`G7K@dN&T%J}=VSC8Ul#&)9) zAVtRJcwpPHVcU_iwWe*YNn0!M3VdKL$^MfMoc$Zl{(C#Q7fjN&k4+Hxdfr0=;-66R zk0|-al+caEe@+SM&0{Js^%s;$$#J^*QJkp3l+i?Bijp$3WtbM#H%a(ZrA7-+RWc>j zhLZ@JL&m%?2Nqah3w~?JjA?7ZNb^tz^3>(Fj`Aes@nJB9VNxd2$*+AzKH3W?;_Kg+2mkzLkJoXK0$&8>~P|v`eQ&rBp@wREY zb%%gitFU5%uttK_ydz`^nLoB@nO(4+*4v1(Gi~Crb~LRNWGh2Oi#kO;*id23o_9NB zvfMncwFwx3&a{w^P z+mYT}w;Ujc7>;5VB8AJvi{y|kJJ1$+QNn0wE*=Po&G^kE)!8CR!Oun3grMPnd6$ZR zM9C_WY?0I_+4+)F>RERI3tC0oAx$w~fjU=dmC%x6K532y*}u;_`C*Mb<|G@I?s4?T@(Pg~lvYx%_Ti7(;Kfh-6@PrrUT zS#$VjgUhE=j`L{;QJ5K+yr2$hefj05wvzALU$-Y+`|j^a*^Z@c$Kbe`an?L=_H8)( z?p5BKPdN{gK-qRMMjX&v=4#Oa0maDDpNzC|^00!K=h4@CNeHC}a-MN)s>Fx zilApr1wG_a3W~94T0~@?kImYE7SwBfAdEF)hVu^nm}31~N10WD*n`T6@~a%aUX|f1 z42`WZSjUZrKl|T*^{ZcXlyNGVQP~%8g-S?}!q2Rgv|qC~#fwySiIR^g$tkodj*-cn zvq^>nCF!_==2niajI4~jk$B+rZaBSb)hTC3 z+S!q`buf%P`e9K);^3z=jQ@%f8n+Jxj=ozd<1gjpc^a-$__d_`0YVXRK#7f!-4&KY zn*tJroTZGX45gONO(8$wccB~n7bFtOrFT9fO4BFN%h11sY$T&4HJLC2d;2X31v7fb zK;i2+k%Hg}Q6XqxgaDOFhe8NIE+-QLi$+*UtI&GLr1mn<0v3jlRX|8?cMF7U9%cCW zc;e*?QZz|YIYM?>)}~ z#xYOdtNHGKXcD%s!1iz@kfI*K5cKb&pW+Tm=sB~l5$T?w0i=kIjBWnVq{GVGmPD4C z(bP+{=-*r+?E5awLQ?r+(b^cIZBIIKpKpL@;YW@bYexHd1G!8J2G{{;$+Z110|&NkL2{ON8o8!ZAn`977q~iY>4W%TM@CsOiEe<^W2xd?wm=$AE37-*_mIx{jM+TTAw+zA}|2jZAj4D{T zY{fq(f|z*=-7NfpWS1!U*O0wNu9-gHE?93MZeIk1RCK}D;ZaE_?77IU3JoOwTXaMu zJ|`!q5dUZT8KZ<=evZ=-X_iAC%Zx(c^>`y|k6)8SDp~UxWQa63p@r3$jRbfla*yPK zJtq0E>S!!6O_y5c$^TRQk2qHhu61;m4uBP+yM9WL`2`R)JQ9yL|uKG_Kw!H&W zy?yvmy8jCS37!QBQHW{hj-<0IQ|?X5pG;fdAAa{Ad^gp$C*8J(?6_JUwjKRsU;5ZY zs%pSZk72DgyVOydfz(adhh6#*s+OJH=SE#r`C z(w`o{kwbOA5HsI2atR|v_cq{5c_{{OivNIS8ZAkAmTlrZ(Wyt1 zItoWMrT;z6wco@=MB@u(t~Ub281G^UtRNq_J2%{&>q9B`K-xV(0yJKfs|2@Q`z{dk zc?G#D?2KmMSJsLVvA^Wx6Xv291!421`0}6v7sQGA^VdPkQw6Bc-3Pe^Ia?F%qB}G?81i zJqP_|8I@>v8DA8j-HTPU`+qdwhDETps z0V%$hls^?I4@+Io(o;-#+Cua^5`~_pj57v{1*=*~%1Vg7SCTrx)4nuHf1JRl8;;|v z90Y#ocsz(iOW=pi4#95;TMMY0ikP~|CaIg8fpamn95T_jl^|)Dc6k`S$}}K8mR`+Q zIhoNDTDy;5U|mwX?}WZ01ShC`@EHK;@+0Bu+)_p@9fF$)L?X=FKfwk3Umhc5D z&-G25)PPWfT6dtu^Wrni2v!B)bFGAQPXQy?{V;-6F-j14`oMgV{hxGf@oWkkCD)_> zd2Yoz8srs&EJQ8bYK{Rz_;D4hs2Wq%nB?vpvQQ`^{o952oR#J4=p*D82#5h~C__3- zG9}uRrliLq|u23iT3sur=0gq|ytNFs;zDboPgoY(v6#5F#7UOS!P;@D_!)QD9~| zx6S!{b|z-@A1GV#;$g?Kzq(KZ=%CO{?Zr^PSk-hQl z;nj)v!zp)n+6^NpWNaF(uzTFvR@7VyTO>Jo=<$+g(SxSpji%vEqoHbyVjeVm5b~he zL$+cK@L#EJqzDKU)V88=(}YVwN&liJlqaF5wWQvDgG09p~v)Qn-cV zxJxmZ0Lgd-TbE&1GQM@(Ts#8r#b2SbS>sGBTNIw2k0AJ2)$}OF2UFFD($$Aj&ckWvVJ75JocAuL99Pl~rkN&dmq)gzlG@KVB^gP# z$mZt%gp+SiGOCROX@1b$RufQ=y~=A-C`=COXEd%*X1F~GNpIV8zw*cRP@FoClj0QV zvgwi2_5H-_iPbjk1`neJwLbqJJrpJ9s4rV;eXPu~poriv5(WNJhRaOm#Q6z_ztr?N zzRE$fh>pkINVM=*-pneDt*pYfFt)G?mxRpkSp5#ZEgdsR3;s($oQrIKG`4S&;?&3X zLy|X3qQ%Q%t&mQTE8Zt&qS!!&*TR-`6T?^KN5Vb0=|I zA0t4oO4~1CPd2u*ArFefI?QZ%XUX&(yYNA|Nl%5X^cd%^Qw#4L}U&bWb1)gjo5n&8;xT7J#KD|jq~u@a(%$2b%@rFmr0xl zP?$W(!?EDh?C{lr&d%@fC%Mgq7xeOIlhbJm`omZiHe75QEOH;~>gP>_X69I|Gcxg> zqImPn;2S~2ye!qH1p}|dO?oBsP1%lIzq6|HApEIl_b?Ww zh-Pn)g~O*cmK*eo3$qb`LWDnI^J#|7PZ~mUm7}Of%v^>qR>?`l+kW+G^T1g1)vG-E zQw$@^uFq{IHhyg;GOl_%nAnA9qsk(lg9Y?cT2nOQotT5o1Gtm7hp)tlh*?Z~I+9r} zjRAlDZfS60a}&@3NznkI>x#hFET`+5hz7B>BqDF%@g#N;{iX+Hg9RJ^DcJhw#)jWL z$ZyZ0S}8D2nmYV`m_980APc0WXx8X%SYY7AcxXCIl7TUO;|siTu%lCO7?8$joK^r$ zuTDntCe_n^4-1jb11D6rB2LX z*Uxdp+mLUrY~qVeMfe_Hy6rP1z(;f?;zjaJWckK$xs6AUDtm5r%k8oih~SZIH%;Y2 zm4qM)A8r75xK>Of>J=9tr-sHB5i!SM~yzDy#a>3G;$h{$~O%t zJbKvXZ$83o&M_>ABus9p++~%;g;6aq78Fr|D>4S-1Cxu_DL-URv~bnu#rnN^RqCHK zpt)H`D~cvcn0Dx8@y&o{q*y-;dTcdpuqz_WD_O?n23&^42;(xx4D*^&%y_YnDWWo?d-Q`%Bu+bAy-J7}(}Sa8$#F-% zDvD6dU-Jfs_#4cd6r`v_u&UpeZNExFn1|ISs$Tm~wqP1=b7A z01FVfl4v4qID%mx6A{>Ynl1KQw2*mAxrKpv2^a%ZO@yhx_sR7-RaOQKW|OYw8G zqqLAH2IisMODdMFk{zaagz^A3*CJl`FQ`G1GcZ+=_=wJ!T!!%k%k)#HS7KeDWWoey zfn<71%HO8(uz~E5S$fGRl%FyzorI{Wu_^zn||1W9d0+{afG6j!%fl#R+8v9z;|FrX!g)`EJgUP@+mK6 zBYtg^%?NB%X!P}E&Dh&|N^N5RPa~plZq14Z;a#U4L!VN#L34*(82(TE zEeXk*tN5(7d{UV_8%Q3Z&uS(AFQu#kn;N4U6&5`ZJ;-A#5=8Qr%_5{e@;!=31g-9) z_#rY9JN3OQ90uY4pstcu2_k1JVwx|TSvLx+XtS319Ac01b^52cNc?k3eo6`5RKJ)i zXem?jbGl0qHPnmAOlygfVxh8jWj7zC^_}U?Cvcjoxtd1mzM6Jlg(Grh4V;R1G7jBn zDTVaMQmSz1at6!g>x=F=j@v1RSvq?hLW$mYH6)NJX4pDm^kchL5|D;<`$EGI)9S}I z65(RIEupT&cEmX7IQeAFMs0^m3wbIi#u|ISj+APm&vz|R|1$=(_PSP%% zH8SttZCFp0*-{O72>xe?zSuHVVQhumCLshgQnNGI^IXG!c)PqO&mK7zxN!0ag0phc zh{&S9rJ@c3X;&F1^tGfg;0V`c$&wL_0TYd|K_a4xNDDB{U=(E8hC*7x;E+)$2#Mew zX0Zo`C7%pVrsN^;9A!M@lj8CgrZv6^c9%rg5|XD4*;qt;MW8ejrTF5UTw+f{&^aey z;R;FWKhYhG3)Hrl*LyYTw?|FN90wYaBnP02WDc<#A_>nb30yHI(6MpIhMme8x!N3~VTbilWIWE1 z*FQzO*kc_XjREhozM^-+Ro2sA+T?B}JcYZ<`bNazmrKGAY5P;9XX^9+(V9Md@xfvL z#$kWz@C)g~FYrQG4Wc?k*+_BT#mEuc{JI-UhRpJY zwa_0JezR_?z1j6Hw~MBLj6$KKQ#qKM@k=QQ8E&w1u@$bZ4s=sGB=^!fwSx}!D5Bj! zpvA~t{|CnWJv|l~!8*$3S)xcK|1Wuz&29toUI=f)@=DAV(kSyfKEFU@Uo?@ze3 zlZD-{n`ZIrxCArhQAOX|Rd3&38&6eqrs>m{tk{dsy+Qt;B2>DPCEGHMJ03LlZ#4Gb zYfCi_ryGZtkH0Yry*f;M$$Yrtjf1O4HcA?kC5>yXKis>~yerwf3%i-GbY`~gSPid+ z*Dk&H(%MTfSlzZ8^UFPispcNS+$$ZT?FdTQa!5E^cyD_+VcnA0Xg_o-7wP^b1#Umz z@}j0Dek*13PqZI8e?S_`gD-@-I`I4dLZ{Xy)blFJuaJ{KzBlJE3)58Te(7p+s1 zBs)0x{7Wq_erG^k+J0zQs=%XM@hZFeZ+CPYQaaV5oF)GB*LWtnJ@57N!7;>vS6ZWj zzmaZhSO8GYoCTP`i56MpHa*lYSAyN#ZD*@}wV_y~mf2ANMI46$@DK{K)!2T0j)z(c zV14n>O|*~kB`UST@e{fuF@PT$7K98k^}v%$P$w3Gv?K`SkcD3bOjg!gR7R+nr;42) zcUq3UlSnx`)6UMMt&`1N&G-=SLQnGe3{vg*fYXBl_56EyhE*=)s9GP zyESR%z@IEF{z$k2<0!w|7Zh5`s$58y6#(5aQfV@UcK)?|p1852+`+rt*Y z8r*bFa6@dPh6uihhmmB=}IHiV*HHe^a16wh$?Lr?8$(+}Eyziq7}Ro9)a>rQ!k(w?5>6R?O`t$6)A z%iocV^?Elvz4uHh&tTd!m@FRz5nNsmDNaS@9p{7c){XL3Y+6^|l`ijEK9X_52i;V< zJ5yJ`QnXU^DYy31HGr6GcdOs3Uad)0G^Zm z_2Yj!_M^x~_n~C>A?X2q8!q2^EalpjcI`?!b|L)Xat!h0GA{Sswzt|=cfQ$sr}v@T zvoinY{yY2S8+qQ`erG!Z$lZPJt>;!Rr>b_Qt9EXdSj&q)7m$2G3B~#@<|IqAj${FY z)g6DLHP$UY(Hd%Ljp381)73$#T&7`r6fW$a;e~4*n?S`9^(~0lXD)|g5to%B#j+AA zB}9VX3SwDP04kNrhuQ*73uv!LaBOuAhPfz_U{`1?(x))Q<7b&($gB+3!F+}r80eYu| zpdK$FVL{!9!x7f-EC0}wOz?iivgU_TP>1PJBLsbzw;UP`8D!3iyRU)I<~)c++(FQe z!Nn_RUu4_}!f(=U3xR-aX{B%*JeV8JCJ0R=_F@<<=*cO$fwyR&$vO)hhQvSAZ+}qV zvr*r(KAEZ?O4ko9kGydvW9#}I%kNBK09g>$w5=;?+l9}1i2qm8!sANw{P9yruvTc^ z4*kB^2s&jb>~*6L zr~#Oxf;b_g1gDzSzUKOQU6@Gy0G<5^Uw$C!_0wosfk%vNH04A;C{y4Q-nUatnCLBN zX&+BDyb8eyVvDEKk#1V25&d%j3RcPeC308`O%Yg2<(;o~AJMPa!l9`2jy9N`u;qOp zH{|pVHv9#DFB<`!r8_S@ zsOsFP>P%I2r>nXtN;I|?sjRuPZ+V0wLLGhM)@nSxeK6^y53Z_4Y~rDmXHVL*Ct1EH zQ+4F+7uMR>NB;QqADzBmlu8nZth6m2Q8_vD=-6`i2Y3CD3+Y@{hVsMp1!2z!ziwE}1!744B($1hgWT-z6Al8oel^0X7{vXeQ*)faKRqYRp)G3y&qt%Op<)0(%~&nW$CB zN*>PYMw0Z#kKjQVQULmWw`Dc{d!6rK$CmDmlI~*?b8jo-n|2;ZI!Exi zALsv*wh=bO+!uR$AtgKyfcPA4#U*GYOBG4H%A{&p6Jkb-2k4>MVJ2O5Y zYXpg3M9LpnD2cTFve-)ri4a60$Hm?BbC43EI7C8*9H-LE7imDme@@XQ>9Jm(x#>3pyAz(E6sP{wPWAr_*}5NOTVB5VUlZ<5*wA+s00a0Dy4wLU8Gd) zQf`2AsVg-UDH(}UL>YrZWz|Z5vT9$-J)CwAL)Gf8q~HDA>m5U3wvzF)lGK*VIfHW;LEWP!NJo>kNx?s3 zd{3$r75oQE{w^hCLZ7XYjh~rC#0)kzreNM8!T4;6Tn3{yk;GLj7zf)3S2R)=9F#{A z1hI?~7P+IG&Pe{lU27TZTuDC(35p&h*-E)x=I-Gei%e4}DMEw#B3fL*IE(PX*Bw## zJXm4_oi$RjjV|*l;=@Dsj^X|xHNarNemeVml)O#J zA5p^mf_LI9TX}&5=`akDe2dOTZbhcErLu*e^b=x$voIMwMHZK_tQ|i&C!a0C=>gds-8Px9{Y299 z2ae^KLlC4J44X!q!HkuRq((68NeV;xpGQJNQv1mWbxG|fBiNVNpX#J+iTx!#Z7bNd zv2ER!^7N-Y{VS%7P@B|#9u=1@6=fXdOLmq#f_J+!BlwcqPe!<$-1-Cec*E0JZ+|OO zt6=C%3LWZCMmUhvelmhT`E5QKVMlU{56re;Phv8b7?+AR%Y+iga?|pYueHC?j$y`+ z{^d)r?R#V2lI@%Ru$M2A7=et@*vq^|7=M?kIH87wSvTI~@ju$j(nz8b7DpFLFhldksJ(`N_(QpHuD^ zxuS$&y8!_Z@{^U@pHuD^xuTPXV+KR}rcgMg`c(3Ty8OKHxWNEV#=_~!)aR_^3w8M= zqssvFlb=v+4HuQrC_fIhS(VrAC~gM4K07 zW@uZgl&FChp$)fD0(-4?lR{gxsfwi7?4rQtv3)DBFQmbRg#o z=gbUA+0JFL*q0qqN5gaZ&wtMO&wu&O(XX4E8wET+pZeX@J5LM3ztR``sk2`04~l|t zOHhQUpooeqC8k|bSK1wQi}c-{@}#{{FMIc->Y{ZnL9IJqZxc~{@1gbk_~lQu0d2gg z#&lD(iM6Rq`P0qO=5$N6B^`(c(yh@}mS3N0O9!Jt_U=oC((Tdq^oHn$bT}GLcSJkV zozc$p#^^??-%aVwahE8hw?w}s3R8m8@Cz~en9>-1+$9_rl%^|!;#a?QUb4Bs`&Rq? zJrDT9FMp!jSc_(~Xi>Ml=SCTRt+Fmw7C>1mC~UX$*VWpgc*cd^wz;9k8Clm;$;rsH zoKaG$rbpsQJ)V{N`=ul7g~~5wHAR}1^=Vaayhjv!U1t5XdO4L$%$g0kjP=?pn(nl+ z?+pHWF3DP^*JHM7$*E~0HX|!B-O!SmDYMbY#&VftJgcZ?v!>3dvSAnbP&7Gfs5%jv z-oLlE&aAhqiaE3Od^VY36=E0VR8G~+KqW^_scGG8O&Y3JQ>8g4>+xhV2GU@l*KIbi z@x^A6m+epu`aYem~ zJ6ea|dc6D8hVx9Ksd=P=u-YdLNy)0ZBuhHhE~QFZHU|RP1d|42^+`lfQ&UOQ%4Qf};6%&F zX_fcKyV2#85ougMpll{JD}gs+#%4j6o_tCg+K-_?5QzM&F^!I-l&npu8l!M2nM$Eg zJ(n`1WCqNQsh8tp6R#2p5@mdgoVCPuJUOR6t7%znLP{oBCzvK9D~+X9V;XH7X}|{1 zrza|tG^t|DDNHt4)YS88+(5&xAaPOEup=;c)<9LK8=JLcqQcq^nX$hN$ zL`)lFBuFsF*?yMd*^D74Gt`_Gon(XRNpd_6S)m>vzceJKMDUK+Y&p`>N(5$BDFh`Uj7&V+&;>p#TqFzkKRmc~% z6{JY#`v<*{q&j41n^9*~OdiQ*FsZtln(gCb+m+7g#x4o+l1(QK8dg@5bQL4dN)r>? zwoO3t9ySW6lXauHG@Xo3W7}nR8tD6?ip8#;v+5x^Bzp85+F&fi@HRCgPo~su&J6Wh z>_;LJv7XFkWJOVxu`y{VvY)SVr6>+VFl7~vM22`Vt&Y8;bm^e8KP6dHrA+oxWrqxa z1q~T0?KuowH&mG^j7gP5#Mx4YmdnIR2>6bp4lrM7*}ypX3QejQeKM;>B4yeCu)iO92PdRUDp*P+FROfq5Me%}aT!ymNHZE}lEzYH9Xbo0 zVq2*iSdntHvdKh}W{)j9%CU5DRU-+P;?#ia#e1#L(dUlEU8KUThg=aie(!HVdP|rW z&f6uzg3F*1>zNk~$CL}gd!X^YLr2>%FRoP8wc7}AV``n01-Lm2Zlj@6V}4x?Z~!C1 zp9`!s{X(7bttz_W{5raz>DIjSNO=#&{ibyAo zojQ~<*CEstUsP=3Yz7`t8j1jfsHSuvvcIB(CUeXf%#e3Ml`NaXDs$pls94_6)*+SF z+?XViaL||mx&YM(pGegWDBie{)tE!0z|JOhgE=+iugXSFQ>!e@OVjW>q>L(SFm


vT4Q^6e-PSG80eblq%;`hXW?1k}!=9 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 E1_L-l+5i9m literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/des_crypt.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/des_crypt.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..54b48a4cfad8cb55dde16e0e796d7133261c88ef GIT binary patch literal 22513 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 literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/django.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/django.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f985671a470a50df49e287e5cd3f1096faeecffd GIT binary patch literal 22705 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 TgBwG+Lnoe4`Li;W82|WROt&F| literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/fshp.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/fshp.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7dbfa1ce332455f26c10b7a4978b85ac109b5318 GIT binary patch literal 8645 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%0AeS1;q=MD1N}oqv+)nH zh){EH&-a^!VJ3{Ms8;)4(=e!XVX&s91&lUe+>phOQW7eGXLVI^(c}Vxm1Nk9!Jv!| z=ippvnvToNFVyiA^wh(sTc)#M7+|b~@{>DOCnuu1jR&19VUb}L&D^F1qubErQ!*b8 zQ~%d+7z{1twOPeXd+AQgQuE8V#k z8P(KkL#fKt#CC8j!aC`*%9^3l|H?7~Nf8(@GY*a%)NgQ0@iIgRj1nPp$7?K_k~DV+ zH)#kgUGizD7rcvh!=LWxRpMQwxy*9uEfuq5CLG5}LVunN`A=|P_vO9*hn+cZU*6k? z?puv_NA9GT(^>CZE0O!LCp!in?-+R0ncFdx-!b&NgV`N#WnFL4SsnL($GM|uouGae zN$e{fCb$KeXc00zf$(G@M-hC4pu!UfX*eb19QON9;4Nc))@(jJg&^o5o;Emz)Ni|M z`>c9p06rh)w)f<>_vGq(^Yy)%v9BDhnG1PG>*~)Qoyt0fbB^J>W4M6MKz2tXx@|NX z&OrEg1k%=6OsED2ARXX3& z*eIW#hf6EYnP_5)k18^H3hRhfJ3+5+T$1@D++d+j7=F9P%xI1|>hFWD8RagKtsyBc zsUbS?`o$#khKPI%GLPf*>XtLuC+_+??qzr8_|pdP5wkfa038r6ISSsEmF7Di!`AUN z<$WEi2Ol>7%}2j${i}s97yh9$>pQO-%$As?&O6Q}XTjx$AVXKh->duFRZY58gPNfF7L7*MYWk%?$ z1$Q2Rr^8i<+Olgw^ooL3J<2N=_&K!9X?1$|q~Q&%)ty1-2J0S??kGszvZxac8Y8Jr znkp`+U`vUIai^>7{uy263Uc$U>!^N9<4yS!s0vVkC@kz2a6o35H5vEdzJ$1>(-o4_wD!da`7H>9;^$%Wm{7 zkm2m+-}OBzkloo$zH4kljctvXAmd!?@iB18V?9{u$Zy;C9234SrgraTz+G}ZShfA8 z_Mx2bJNCC7`4f}RF~`@%Y?m1qa~3w##xU#ccutePF3r&7V#a8ZjbOHZHx?OVu!xnR zsf)pFyD`kR^gX9ZUzcVWXS!*bjbV06|8s&#UsJfI!^=z;GfCUo2xeOcv7JdMQ<81s zN0vdZ)WvKB@4ko49HY%x>6aPOa)v?PFgOA**L*q;Vd7h3olNx_u|Pdxwq;3cj%?49 L?OF3niPnDuPSO3h literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/md5_crypt.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/md5_crypt.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e6a1e2e7f8bd8ad697989fbc59afce25adb6d11b GIT binary patch literal 10144 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 literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/misc.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/misc.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..296cf9c76b81447b783b43a9ad189765baaf1831 GIT binary patch literal 12483 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 literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/mssql.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/mssql.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..069c51982d4d1ae5d6b0a23ff418e879fd3d5c4f GIT binary patch literal 10146 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 literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/mysql.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/mysql.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d794acfe6ef3e61db7ab4b19f89fed0bc673ff4f GIT binary patch literal 4810 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 literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/pbkdf2.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/pbkdf2.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..502602ec0b45a71db9d19b86718565e581304168 GIT binary patch literal 18781 zcmeHPYit`=b{@W|7e^u`*|KD7EK9Z+OO#35vMt$(Z8^5&hy1L)OJ|oFaYhnlKD0CB z+ESzqTVw%k8lb|ifhd7?!)>#!jJSY)q<|Ml0k>$2{3~e;YGMGR28u=Dwm;;?3lu?t z^qhOiA%~*;*ldB_tw-eHo%?>Ad+#~lJy(BOQ&Y*n^Rw~K#*dz4n198W{A15KzWmC_ zFdr}i<7WheUidzPT`4qnN z@mE4ROSmdh?XMVTZKm*liF-wN)ZHYt z8&bbkQugz6((`~vFkfWcJ7>D4cv%jIMm-aJR0xZb?3o(9CX8|2+_|Htjve=Lqr5B% z+ypOAi1K4>pxdb0Cd3=z(AczEk%;D;;9D`;yGM)$V}eMWf{4yxw?VCp#{!9H2wx#u z6%X+OG&J`@Q@PD*jU1N(5E}{&hC-?#q1Ggb6FnhJwdM0Ms8yt*zQ9C;54x?|t31Jb z)tX!wy!iI^7QgQG`cyU;=L3ZpUbRjLhvG9jFXyNjD;DQ z+LoIzs)aDL+akqS}KKV(^-rh`24PF)^W5O!1N|1|q@TswE_d(YS;gh-#&L zf7_y(f?-)T<0LD}#T1;lCSxCS_w9IK3>V)sMm=EdO0lOIhkZ^&%%DqFl6r%!SE<_EH@b6#HDo{Fyo(`;Xc z?Nivkw7u_3D3u9nECnw(xd`h$0*9XUfZep1diF?ZYzT?8M=qC8< z(~w-=3Iyg6PL!pC%Z)4bpEcb*_W7wV>Xo6Z8TKuOeJgE$i}Z=xrP>35C?6360ktv^ zh{S|M7~R!@!1oe-I2U3I1cX=+y=5^Thv5udyDrF-F{%fTTpWZwcVO_yg^Q{qUsVNDi-BnWpBn zxglG>F?j}F8aE|}vvtno$!uLS5a*`kDez@o$rG!a7;D|C(PHUeWq>>}!RxzMsfWX4 zIbv94ifE9uHkuAzFYY@yc5L>WdiGp*daZ|h=lc-?Z#ep;h;!`*ryDoF_ z36blQ#Ie4Se57vNn7jF@(1@q95>`%bC=LyonuVI77aQZ~B+2D;2@Avd(Kqsgyd)kP3wqAw>Nf}_dlE9t=Mj!hBaROS#VIHl z>q+Yjpx|=Q!$$&*A_7A#h=M!Ue}(3erMiy`MdPI{q#;^Mv}P8D zgmerSk8ueQrms&xD~f7>y^HLOy&@MnTbqvUG8Zv7-T^f^{6KVH{C8wf>Vrbv8rTALq5!-Ti-n6jHXoqX?C3O?+a|g=5Kg}z60qZ-7eZ0!fRK>u22@gFQ4uEs zGH?utq~($4UCIcOD8se{>48`J#2dlBk;~YVZ%R^38u4(zkx+SBzeAU#gg7$NrR9VZ z8uJ%1a(*y^F$q`*SPHq1=BQ0N$e&C)5T$X>UE!rDPC6~aJZpwzIRQKwipp`37d+em z&6CV=QW$V*G#E|@BG!(mRU?I5;uCzF<0TPBE5?B&59|)r!27ty}2M)N)EKml{eP%+&VGTOL+zNgjJx#ez~2Ul{*sJnL%8 z*3@MkuB@{;yJ2&-acj2qwPo*zbIa#Hd}pODv&FY+Y^}5Z`gI1%&-=_Q9 z+$Ggn5(s$544Vt?zBqUU-sfi6{C-MHYhRNJ3e=^7cYP}GXeW|ycOVEA(;idK_2mt4 zl;9wY-warY1#miRso3MU0Ulxp{G$T!kV>&?vO1q5AR{(FM(lovP~m4qf`sS=6%7}u z26V(F)PUgOkera7{|^uq%8LdA()3iE4<=xm?T~ZOR!$TMVo(7fp>yz+F!18SKnOM! zemcL~QL&(~55Y3MX6MHZAitm%?Xqw1lTY5*|h9kQGqnsIqDXpiGaf6reLbu7V2fof2T`C6U__ z#Wzk)=b$F=+yd?HQ%(IPC~l`tfn5*9eepbt5nb~d#nm!QdJaz~sLX~Z5Tb-A4MEPn`mTWRY4tJw^DP+<>hFzXu+ zoz05_AD{f_B;c?uJxWW@U$I3e?6Ou{!;@!&-o6I@;KSTT2P&Go}4z8kW8bd64x9PDv1R zEkLbOAG{VLsstOH*;3jgdI^@3?dmq0>%VP379FJLO+DC z>10P~l!&B128a9!kfinpDu%_r${;yuV5&ER{^IfhvRPd-e`J2*N2lIDm3%W>-w3#- zqXgvKsx0JWe3)PV}%6kT{2dJ956&4t4OX+1d_Z4gieXaq9`#=k*u zz$9fQL}K{?V8Zf>z_IK6G)SMP;k;Lj0wOmKdhjcSARO;Qw1MCTFMthFU_vM37a@D) z^noJm2{AT=X7KcZ2Q)@P#Dw@r;<$KYiX(s&peI*7J-fOO9O?G+-80`oT#5n&0Kb+| zDL^dOGW`7p6JIG92%o=#5Pt_CM516w6R@Jrcu}`}ndi`p=^~n(F$$&V<%}W;LuW$* z7IsF^MoJ{Kf;LiQyRojM|9An!7q^K>adea43guHV-&88*uPe@PUSl^@EYKf-B4iXG z^ac~7+5P?ZF0Pskn@<{^Fi4*gs!8^b${RsY84dE(9emdlJR7B$KUcxY;~!%~sVdTzv1){Gn_^)BN#*Swa3J z{Y?7Gx6`aY!}=B0pSJs-(RpK#?{B;F1Ta)8WzdU^6o;a~>|otqNCv$e*k`Qmp`ze7v5&APU$l?D7)`V1GwgYVJ)gFpryZop@sN+kT~zFwWm4d|_R$Mw zbH-vmBboxt)dIhj>IZP_jpgK?*%Ey|rEz0E2L6^}$y7H(9pf?KwKi};FJFfrs%?<| zCYCck3^EgQ4yhl*F#(vrk&`AFOVf@Zs3O9Z>2saM$8ceiKVn@E*w*`OYlhvbuv6%DJ7)WNu$auKFC`yqxjIXjT;7k1)gmP)-~!2oN<840;e26;hY0F zxKeH4<7mbi;-ke!J^WEvpUbN}RVL#1!H;J*vo`k-s`T&)Idn_~E&x>;1M4!wqDqpQ(8J+^K3TAld0t|A`@9QwX0m#5L#y;+B*oFh4XlX^~TQ%~|9qMPv1)0UmDx zfyPvv5BPvzvCo`kj$-WQS<4->9s`E;4G?zRm!+LZu-Dy2iUPKyyFx-KfS!m~%5feH ztI$4$2;!;x{M8f`4O>o>u5O@$iU&Aa&{yQ+pEbx#KkqC&{E5@e? zLJ=Upwd|r>EXBVKVbI(I( z{mtzUobLNh_wu%k)1x>&>1q!Faeo?2b$mFvG`Vb_PNT4;#Tj9-TZ^Vj7*&_%t6Ie@nTC8MYJq(e?h!Plr;% zhi8}0K4|gXZ}F|{%e3?>E&VVCAiS|Hi2U&z z6`@*SR08 z;XD{_Wjc1G&D)b_7S24f)UFzBmabI>h$aN0htp))hN5APfJ}%6zU3(u`mkE%g}xgQ z^3pjBKg zB2cMt^Gq`s#F%mqTwp9K%9nyyYea1`TwpBD$NU@R^E`8xuM8eAb}`A~)N_qos-Q&@ z>_gPjWqsS0Eo*Pg7R%B9h;y^A|IUi6iotdrGh2uEo0Hk}OsD-h(7y?EI(rQz>CDQ=uQlX6ai<+L3f1 zDI6LQ$d+*4IyN&cWw=cf`u_=eZ5nb!vzf+)_|0hw$v$8^@3Wn7=`?3Tv+q8;FT);C z*aK)l)36}ijAR_`ilhC3W9NOx&Sfy4p~f_Q_Z@v1$3ew$u#73qKb*fO|Lx_!zno^l zHs`d$o=)3OKSN(YP*uMUmFf%60?a`+1cRDU>@BkV#n4q=Ti9Bri}b33g8j6G^b?qz zfvT_{(ds+jzH9!x>K-Tyg9VgCz~#1VBrgM`%ftc5KU3w=31{dHtcf$e89)Lz~jzU1Lf^^osQiY0iKmVX|nn z(qtJTKIJG?EwJkD9%65#+DAiCa!;Jj0G!~e6>SWA_fQiuf@avlvGH-ZcA{3mvCnC6 z!?g-j&;aI$jl+L20C*VxRYM{k3d;mBQR{Vn9xO$gCb!GURpHWbxIZ<;gPjNDNe!P+ z&6W^LQXBF)5qtQUCwD0pRZ_JvA4W>n{#69r{J@&1_Wadyir2tZRJhmzS5Yefthqq0 zz^Ikz;EaOWv zJ~)Ulbf%ddYd_g?o)9g|IFjTq-MC|69IuYpGmT!Q(Ys(;wK9f|G}B)E$ubAh*iS+tRgdsr}0rRvI$3`;^*!$(nM0k6hape2QyZy1n;q)jfau{MB@DJnfpuxF!_W zM4GA1HgqgpP#QYY+xzaGNuPQv{oPpl`rGM-*-XQ%(lDE5oX?K|CsDNx$?8?}6@vk- z8(BVbh4g0B23*>7sF|IyL&qsFjs5f)iOAaazl~X2(*OVf literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/phpass.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/phpass.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ca13407bfee758064b9f88d1f0086a8a67608d6f GIT binary patch literal 5287 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 OdB{e7`-JiA82$?n6MOgo literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/postgres.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/postgres.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..53bc256250bbbe675edcafecfcc9b28a3a3392cb GIT binary patch literal 2271 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 literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/scrypt.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/scrypt.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..569f243ff96d4269cd796a8511cb4910013520e9 GIT binary patch literal 14032 zcmb_jYitx(macxf?e2EByKQ4Ih60SijBV0j9&sQH;E<4jlL^kN$xOT5Rkqvo!>Ovq zVA(UCh|xA#GUH^4@a!bJ)+oa`0%_4~q|J|AX=Zk%jaDOVH>y!uC0QEnDnIzKk|Q!w zlwbRuTaT``A;U&nlyBd<&vWlR=R4<~d--)R*udfWyYY|5PaNX7|E3S~VLAA?Kjh%J z8=Sz!Il&<~Gmfk??##O4E(d+PGVZwB$%*bsk6BUl-b9UC_|uxH4Sz2ADDI1ALve9@n+Z=CZX}(NLwl&_$;;zhwY+K6d z;IbRzn~+jJ?ufU8j)>5(ruWS()xRdSgQW)6q;7F?Pjf=>3fdDlO>Q+ufH-f9Z?na> z+u}QH@lG>-(}%ghpZ>(_1=l&QyK!dkR8p2R>CwK4WKPJ4lH4b!q{~wZ-`mR@F)A^g zmjr$yDNo#^`rS^gaXPI`BxEJ2h*>eGXrbh&oX->#F_F$rz4b#b3qC)STS;|+p4?iW1q0DgmlH{K}J#Y2KG9!9)LsE$$K9~w9@%pAv-Ece143V@`aor_pnBiGCwV5GCY1|(88w* zvXam8N+Fldje}q*nH3d?4v&O43L>xMP0`sciy}WNg4%9_pkbmJ#U}$y7?50{t{xedyF}eo9H_bIA;ULA*@TDG%r)9#Ae%iELy8 zJj8|#V1Uu6C$a>Zn&-_1Xcnj_TJ5uPPml5Pl$c77rA48Kb#2U97K6u-@G8@ynByPo zjXk!npJ#$yQ$3eQtJNm*Igy`EV-`_2Sy1x5<6=&f03QOsTOrQCD*+0!fD=I!%=xMg zCn2Y2!~s5?Q`U}2&#_3i23B27MjfpSfPK>^KXPMl`bBUgtoD2^oy8JhPz zA3LZIu!kQlD0~V$OQXnL2Ipk3;MQC)_N4hRR$HfD=cRR|8_C~9w_d~`{9&Z&M49VC zxSY=MBO_8WH!kju_3Yo*Jz|Ojt$SY|Kb@3_bq5TVp;UTa%4O0QL_SLcBSGSmnen`Y z_Ofyh<~=2eNn%XK4F+Xw{MHDl5w^xgVa!Y(Vn&Rsq#-zaNP4qkHZNV?gT;QMke0Cg zm|wj&OZIDKZ#0unU3l=~+p;Vgtkq|kJTVRRK(a=^hE8K0jumn#5+Y(ik!EaUq$ISx6yN5b`l~DH2kNznm{f{K*$y;*(NpBCUuir67sb zChg31xMsK{s+o+KNz2)F9AnS3sIvxa%YD=lPd#MBcJg91 z0oeoMrPRR4%OrkJNm5=K>Ei)eSVNt@qr*}`92wE^n_42yqxMBAY)Wt{F9C7@L8Juq z7A;Ul`wWy}8xbbIqKnLkH9Ql`(z09t6Q^^sA|{1C{-oX}sTsX9AX_e#DF}?im{u97 zwxUcV6|e}gD0mZ$l;MfX@R3PMR8C?)tT!; znoN{3mudPR$o>!pjK7MOnRGS{!w!-mTf>Evq?KKARX+6P129Zaq*D{b^P_12`W>cN z5-S8CA5G`XTbz(HXjf#6b z%-nln1ZfUt^*!2V-J_j~=Fa5D_uZqd#!$+M(0pJ5a(?{X{{zat=eWT+IfZH&k76r3 z%iYA+Eh}ZxiV2=HH|zY+@l(f~Yu54OSCu-mKF7^E6l;USan+4rZCg@E3EJz_ZoTFl zv#upuoA#2k&Lvy>)f~Z5+s18YZI83A1dW|N^_SV)E8D7Hv+i2TXOLB!Qv3BL zHcNBvAG)U<)7&N3Yuq$u3r4-Zynw;?ep{!1!j<^9!WN|AXMGdG1e5-S4%$cX3fJ zH|KXpyEAi6#ACzB7 zsIMf-J&E$R5p~;0Ig(T($x~ovzzqp8iOK!L2ncLu;U@xka7*}J|IDrtVuBd3wsT0@g2M1NcmRHIJ2<5t zd~0s9AZ3Cjo|j^j;vbXpS%WucBF0MD(*6O36Q0;98+NwL1F(tdnea>oezN2JIT2(XTA?Vq*nzT38Yxw+idr?&N74HiAciz}hl1-Z1j z?@r4yowX5y8yrhWSpTfmOLNW|K5*8~y5PLP zctWb1b-s>v=Uj8{IZy71Z>-}o>iAHHcnx)+Av`%=flXjOlNR?a*;371#*yw?jrk^N z;c5X}Oz_lF{fJ{aV~oi=S2yRI^)AsWuX}_w4rAH*EC)*J9#XolmJ*}1?;)jq4=VkC zo~Qb?^JKSWl*a0il%{HY)+;y#?kCQTR#Gi3HD&7r*CUtp{e`kot3zPKlIh5=CL*@h zO@n^tnLT5kJq0()**_ycGU3`sO$_lP_15$n%1Vgm*PJd6Dt!TZ;a zo*7prcXY-n;cx2bOj|YIvSi>O%Ltl>xvD?%GDMMxVrDWh%R-{1(0r*0F?B&MWV`*q ztEo&{(Oht;F{qM`P>IM{vv&)uv<8?z@LiHqM{^75l%fSr4?ZaZ_V@n6h(QY-`T?!blnciQH?pC9_O9A73 zSUL)D$MSS}+aYz^p>oS%wdHWB&QUXw+>|MhonKw>wV&(PU!?fwl%y*8{e5$ z9hi8Ap}Grf0!KCXXaS_B5OfEmvlM@tf@dh$P65HCbee+a5NLHeBpxzO74-wf1onjo zwLtY_|6|sdehO(Af`pXP5CzXu@B)H0MxYilknWI)fUUI%Xr}*}>bB|N7a;Hpz8hQ| zFNeF-aMx$yp1a|m<>7L8pBmn`9tu0QUmXJ4;on*uY;C{#bn(=B6f8%2)ktqC(EDdE z7YG$EUVY-)6N{0%{`Qi;eMPq{|9Nzj3E&S-$NrZ|GMy^aG<*7`Ux>9CxsANezTa4TMPzIwsligxneolit-a ziJL@%{=gOEusekeJa`4$7q03MLfkSh5x3-3Ar3PhyOD%4mV2kx*r@&bhWeHhsJ2G! z*O-U4Oy|fc6*1}HIDHI;sc55Q&RGw))-mCUEC~nT~M$3h=*L zp5I(P*UL;q{gjc(flPdh3ewB??T+fE;~B))+K@@_M*SI?oc`28s zdlQ4~Dh1o->MpG4P+mE0p0$et%hBAE`E<^}O5JEQu(J9=SZRD2H#XoP7BgcyjkJo4 zco>Sh@1tJ7qXDnnOxS=%D|VF1&B3=5o->w za=u_#rcM9vj%@^-XBTNqVw3xSK=7@7>n_u`?f`7fciDaGRbZWkzozP2 zN1Xa*kKlmk8)ACP5>*}O1cE{CA-~y<)$NwXs4{tnrCtgiLm(5;VCZ?q(b5Hc z&4f%fZ*&kNR(=~faJg1|oo{>I9$OH~zIK(~&XTVO?~shCf%&_w(BM){gyTH`m~d$l}!?^W4W?` z3{uU^yALNQ*A0=H4q`u%+G8vCv=Uwp5Q?yKkGdAd8p~yj~3;n zXYU;U?(7u1DVX1bKFOcp>D5@SsN4Py_W=?wdsILV9o#@bI4ee5LV|(8l1(H59BJSlk1l%hM-Ijs zGBssr44+X#wuH<~c!5SjSVp|is=`a|+xh=PRh`j=R6y%?V*GFCX#6H z#YnpLqmpk6qeye%s!WY%*5bzuO5fmM)NxCQt$#@L)}9m6h0=2zOgosu=2JCvt7b0D zJ-35baCBKoVdux({IGD(?WEg(`mmIraioWt&kukr;(YeOk^j!d(}9^)L@ z0(QH$M%>y6-FC0rxO;|hFe2j47milY7TdLhs~kDaA!&{&&5OG^@LzWOBx;3;JyXZJ zjausg#Z6tq`N{4UVD=fx{R9N;j~u=(lu>d>{|BWkh9zr zQ=4K%FV0@B{cv$?N%$}g1=9S_?dAT#Pn-X+wZeC*{9~UqtB*eiP79wU)r%0S7a?)v zS;u;ip?PCg)tiWrcEOw>ra$)8SbKt1Ep>rbn3Hh9_#EALuvP-6@?N7M{H(k0V6C9w zgiYo!4kKMNFIpExvPtQJac>TnOhl30t}09kIN#>e=KWQ=RGBX*GS16z;Y4QF?MB%h zr0P|)VcnwgFjcR)=~UgOuOj-APzQ2CZ{Ac=Rnw_PN&OV#nb~M6DYkO<+EcW8Cs789 zYPEq2gs&a@Kv?Sk`Q-bPD$E=I{*r$`!;4Djz}tnY9#caHN};28KXL27CEroDV!GmS zoWjimy1oGBqMuXUJ8>SEAV5nbw1z|?n->ZhiU$&jHwwv&kyDpQ2>BFj$9YuJ0)|^Z zK_~svTS$`5P+CbLBK22C)Vyu_3|V|uBZ8ft(YW!ySI9QnpA z8^8XWNaf*)r|Ss%YWUgZ0{tGa#8dJC3ieYlOu_dl7^i>?R%T>LWF<)CSCKAKaG8Q1 zQ$PqN{WS%&Mq1FgyO6@&=0pP1$IM_X5BFW&1}0`@GQABEc^9w?Yi9mibwBCxt#T9$ zIk`ad{9q;e>in722-mV<{w!9uW!wDON;AK-s}$XX^agHYd&%8WX>LPqfNO1=AF4D( z=AW%JMG-{W=AT2=XfuAiTm(rd<84}X20g8--1^`P7jmzh*YmqrNr$Hka_s4{vM9mJ zvb>(X*dcoMT3M8!*V^pC<`IFFMG0{StJda$*9w7^M+pNPJ=l<0!4aS5yTFfc3^y6{ zy-q;mF+;0WmgMz%nu*4B!Dh}IU3JiS1io344OFwC5?MIgrQ6UY0WhmeRTN{rV!~^ zW?8Sz$Nj{qB(BF(K0&{r}0Zey#s4gR4qJhvqY{?C2XKpbD;X zV!(tR3@)T65YxghRB!j5O4YjES@qHG?=4raD zCu@yQ)8+Ye>KXk%?Jxw_!7zqyzV;o_(e6p<4n9euW%>^@n;zZLFy>EUmM zLoYRNh1*ej$P1!8!Bv;j@0_oL2PoWBd}CqL)wyeP^MOi(zqqaV)YaYBcF))S$sZ`j zis!B#ymoNjx7zTs#{qVJa9Dg%4f0FdKkT{V{5bfDq#k>r^y0Ak;%FrpQiEH*a8a!< KA5xdC!v6xwa}KZo literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/sha1_crypt.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/sha1_crypt.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..628fa264a9c509d74fae7689e80972c4c73a0771 GIT binary patch literal 6277 zcmZ`7U2GFade(n&94E$39R6~!J!q&QaS{jtd~GRzN!q5liU7U3>MpHE~_~~;V;t#JeU@CWGdnL_@lg-0ZO8d2g_0Zs;nsdJmrOguBK^@GC`*j z|AvkVfe)i#qo56mN|83oEmUQl&lWTtsf-R!jBjk94e=5auIBg@Q?sTv8BNn`dERsY zKAHR^Ix;$Z?8I6>L>&Dck^1+k&B`2}!9URU{o2B*x> zTl6ep+^Wr^)HO$h}B^`wHJObSE%d_m_$kQ@prw*Z!-6e#@AuTr2nYMSB1 zc*8U-)-7v!f$zq!z1vHewon>eznrh}qodG_uP88bN(VYho;U0i}6{F_k5wJQ!>;*ZZ8of!y{8&vd+pz{Kwpfi=%vI2M$u!lbsfu*SxQ<6z^&}6dVq8P7QRw*ia zt<6_7%3py*gONc3w~NL2vFL<33yH*qgp5vvrVAJzi=#!)UJR*w>jMz9>zJ48?*Nd= zTnDLRW>GMiJpbbP7fxb$6gfu)Cfey4rXUCtM0xgW0Jn&4r)okj+5I9{BpXS+NbYfW zV9i}zXSPqFx8zuIF1Z|}NQ#c4v*^0#Xvcy0@0^9?*U`wR5hn}4V%817@QN8KUe*d( zII5$NF*+gYX*7`)4;gM*f}{{}8%|Nt3>Sv?mW56+b6LYPpwaO^ogc}lSvmsxC@^&d zj8_^Fa+(Y(9RYI~$uB@u&K-)5jg4rsPKWb?cv(nOZKM&-N1EAwWFt>S^9x272C{?| zXBYch=32(*fC=p=fD-v(VAt0(Ykh0ly@iK+$^(;?fypXB@AhTSifcuz1wzZ(?ZH+3 zzE}>#DuGxv5G#9Q&tNT?Y*P-_C8lyD=-eVjjyWWf^MEoC*KA7;-Btsr%vLSn*yGT5 zHSwX3;xb#&aRuZsjJ^CwL5d}~pwf=!}&&aa0EUoVSW_a28BoMr@bo<=b@wL8t zJMT*mQUz$kz61(_pIT<@^5k2^2V{fAANRcYUbdD{N7VX&mTNI zbtN@Dcl5KVX*ql5+KeF2O)68DPtbFd-~`C0u_K{TC>{kxOf)?qF%QNG(l@ql8gCx^ zTaN97wHmVBka&MWQ3a{FGSS3be62%`r; z{d<4lv48&~|NgZ@)dQ0i|5VjKRrXKSdbh5Of8V=p<*=!lHUqY!%Sqr844ACK7B4o5 zgJ=O5cWo84n>ECrO$u9b7M-Oq(6g1(O0~$fQ=212=c?@vZJ3+(Wqb6_jrroH3cGD? z(@M#?2n#QpyRg@Ey9NiRw67Pd9UHVadRD}2kCwmXicZNTk>5G&n+h=v?OcRodtq%I z{Z0Pg^XJbaUiLcl8SZTKZ!(sKXF%bt3s3=HmGz9_gxc3~ZztqRBWPCHCoO9;Q*y+` z_8>8jMny(-b3cX&Mnhu-|F+DH9e`hpKX0|Ib_;IWUly1C2>>{HFnnv*SG!jAm0gwK zP&GKT?0FK{4#9A+5{Og-k!4q{E3o|Lx`*^_d)ym+)Eiy9Qt6$j_D+=j6SY9^AHVp+ z7q_oe0)y4SAlPW<@xSj`lfI4Ii~LjjZsH%GKluFN*~*crNBgJB`=_3S`fd(B4()#w z+P^k_zyG_wO6cus=?{>>wUe#`J zcvl5=IcCpA9Be-jVWS9!0JQpRxZN2W%39=GMcU@k4UEr`4#wCL`QF?0*gN>hJGiP< z_noMCPgcDr%ifc4!e#wMe?dhrtlapaxeEZX_Kcdak_2o1QOO2 z!glM;=K|P#Bl)bh;K4+r6-m1w!4JqK+iM^y1e=(IKoEyhY{U$}w0*beNI4c^hgWdD z$*-53dbowI3*raJVk12)xpey>0Zz4P-_zt`_Y&O5i>}S_V##@W% z~x9le>MQ`AwpbrfB_3jrpeI_j&AW>!bbtNRg{$Xl>y7IP*&q+AaHfC_ywGGKTU ziJXw7iG<-xB(kbhfXAu?Xf^R!K~Neq9f^dbiZJZL0mBcfh8irLxO`R8m}xRtF&f== zL4%iiEARNHPs~kEj3-V_pEx^b^w=2qM4=hM#_cn)aog$63fEeG!n+&XgDmWxCbo4U zGJIJ%m$3V=tkLhXz7wLV=|-3N=43t6u-(9>nL-V3qvq1sFpgnKT=PLND%pSQj@@qWIzjLrNBrALr)#|jOEWcp_wtG5%ud&ri3rRl~>k9+$%0bsAhfi0cx9qYsf1C2fUkXWCM3H^1i z-+cl0Xn_OXKF>O7g3=8BSz@eFpk)qrZR?PM9#dn=fhw zT~;)+n6XOZzP5?zyqpu*f)#!?P2sDmpj)Pg&R_;ABivkXzTK)(NrnrbZbmYrzd2`a zYQOaw5LJN*!`r+LT2>1N57+!eg1v=Y2jFqx19%u}6i%Aa2?J-%X9A-Gx^sA8@0r4< zHCddtUXd+}$slh$0!-mQdEc}g>}_@pequqPJqNJvP;hzjFV+ zm2|a#-Q9YC%k7^U*;9VSXWhkhIZ7R1 tR=!}#v+i2rIIx44!&_nv$1@t@f376!r}&U`v^_;rT)2TYWM zNsT;@unhA7!!v${XL(J8^|R!w@oO}Upt-9pHxP6mLCufh9}Rvz)YC=`QKR1&HTg|Z zv)>%G_$@5}yHlf%PK`9gt2cSMQRg7vQ#Rizc=xXnn0@P^gIbnq^ZWntgG~p_9*e>4n&4A0`hYIDp^~<{iTr84~*wyD;xKf)kj;W+LeiXS{h5 zli159hcPnueoPJ^AVcC5MKYaJn1}MSNzlOAZJtJO20G|I?jYKR$RWGFS%Vg_R&SF0h()Rr#kcui6h(mc@IBQy~Wyq3l!Ta0Cy zYYeaZ2-<#J?f`MGrXKrp>PZ%8t*>vLWM4v2Lmg(dc-PZgpVafldju;oj%mj@TCq_^ zIaR}56lM~Z3^GcV8kUSPPfa}(a1x*%@~|3~j2b3sNHR%w5pt3Y!di@TRVz|%1$7|0 zO}!;kbuYE;m&{4?T@+LjmMl=yBBMB{A!$nLYI&))JJYx`Pq zBpp>0asoEyMUWdZYd5rTzGR-TJCJ$0t8q>`z8Y>d@?@{YFsZvo_6sWIWDYEEm2vMX zucD2&;hsrDZENJIuSP-Mp49)oX1m{|h8isEbfBbO#S$gdQOlv9>J? zOWviXq*uJ2^o%m!x=3<%E$t@Mq_8o$3&*Xc-tV%SU#-eXv;?vMf;KDIk5M#lJL^^^YNp6ku z4eEJ~`ppF+GjTC2&qk%QTv+D#xFE$2$)p3S%X7il0;diz7ZfE>!h1o<4)Zlb&jzJg zj;iPqjvAGO$bCWb(K!tU#TjmHo*(4!o-iL5c`h^yef6T=4)x<4wkLy}qB6rPF* zy>J`srQCYLckEcN6qbQ|gQ0uD8A0k*uZz9q>tZji1HFX#<`#ZqITr~_aws01JHOZw z0I>;Fm7j~mL%`$becP4(GvFhk*8ksAOx}8p{+^s!J-Oyd3z^ZCd)fY+=O=@C`@u@- zbEwUTcwOjfRxDv)Ye^2qLV{?8EX6>VThS8+RSbcViPy)+6-{DR(FzYjifbYfiM*!r z@+DD>iy|szB3`Z(Ei8#*M2-d;C^=1L>*h%MUmg`j}NHR;01DTZdJ4( zjEW9fN-Ue{rQ5it!Ql9Vl?{p zXAakUcOTx(?kYI;7ajZ4+6}8W6Z&a?!MeX_-JjQ~zYU{3<4G;0mNpy>E7MOM2iF}3 z*XHwwUi(=xm;ATm1;@>z<7VD;bHm}vbS%%V%%-kv85mnbW*~0`W<0n~VD?S=rFjSs zpDtJr7D?f+ZH%oc!~gV3!P;50cII`b@ptfaL3g@H;w7u|y|WL`X66glwxYEyHM-$+ zXHG6JtSqF)N>)#1>fult{_-u|%R?&=*|ISv=N6;aAKCEkdHn6PWn*`D+Mc%msw%Z* zVJwa5mdwR$=c8*mPoc51U_MYZA9!j$x^6yNF#C#TUupz;vhPY?%ShQfj}{BI_M)vl zb*bdp{rFsJ9HyyvrN-ain-z1X3a%qX*O7wmXi;}GuR985=7!X8$znxXH&as4$)~Mn*IUmPT8E0QL!ZqT>{p8RD``zy16X+W zX1cStAN4-&O&d$i+)wtU9VMG5GrW8?&6XTMB#qfqvbCfqvL_0*wxX>qZ)+>r>>1bc z<@7Khj?FSH&hwjNo1qhRkZ+WXU*4XYz9E}vRCm6=($?#-8fW%`b0 zM}F3}=3X2A)LYnps<{7DVb7^T&r_JpFO!3)X!_65o28W$aEE^%5+G4Tr~w-I?)b_1Q%3){h^ox>rY@c-J&*{XaM5 zZD&%KwoHueRkpG@{-@JdAzrXvD_XCmMuAQi8w~4d&K}IQ7d!`wo&&!(p1PbKEm>gx zT76@wdG8j(nqOrp1nP}uI!dnAOiT7+t~=MgdTXuo$(x1tQ^odEPutJ0x1TSxUnsU; zDD1gVa19q-!%tmf>#nha>srxuEj^MRDVZFZ{ma(0Vav!^U1SX8T3CLwe(+gCV znAXFv+39Q|JN=VA|7V9sax?$}r?J~XI+2lFV@~+6b@k?l`wDw{3$DJRtM94n)Vd3p z`%KYwCOz_N3uAMF%;v<^p@Q{H(R${o^}@RK!sojR)~iM9)x7Si_-)vC7L9bv9r_L1 zsjz(^$f^W1?qGQSHvqm5W^920^EKnw@T_0UYy3Jvzim?S8+onYRFh-ob$$!4_gi^` z-^LsLcHZQ7z}Lxp(hOW{%sM8E(V1TZ@c!7!A4P~uq<^k*918Q)iVhf-e8n_sbM&q%?kx&HAPSDptfept% zql73Hz=tQgnh2WK)Pky=QN4gWs48(Z*`*UDAdx5oP@Jd_heKgGpdMysa&$@IMS<#2 zx3KDMtZJYtA^wKWV6S3LNie+K~SOqv=iShZyc?&Z*O+p>1I(hsi*!!7Vb_J=Ik zB-oTY$!xzh#4V7jSn_z5-ddN~B-k?oI64VD1H{r!Y^FhfqZ*h&(Mt(Xy2Kws1;rEy z0J-EqK(XOz%?s1PL`0Sd5ybbP254+L(7dNfLebruhXd9e2oQ1y0u{ZD9E{@o&|uM9 zy)eFV|GPnnRz`v>|TM6#m=)BYmC2wsnwRIHR`togK zYj-|<>+{j|{;~9pQkyTWE1LG?$&axA4#FePKSzV-2cW3t;JY&*neJAUT%JiVpRl#d z07|Q70$v1+q7Vr_5cn>QVhP0H))gRHj;XA-gOU9M=$|l;giFV9R86o3@OC4R>WjN; zsj^+thJ-=eZ;E;5^t|`ohu_W0t9L&6)@OXaXC(dIg7ae0c`%bYdmuL9Y$*nuNT+>?Pr+Yu|%pbd)cX9>i zm7?=X-gIT>Bn{AxEaz!}b=GO}3RnBK1pHsMtV^1gSyYpnTKkC9CgVtI*p{nYMUu*8 z_^r33f!!DNbT!oX4OU|!(bVphgcm`cskVo_*p6Lo!>+}3Nt@L2y6qd+i*3d)w|a{| zsK)l?_4u+Sj9BH9+FX?tzC2g3_Se06^7`7@E$NcFmta_Bfv%mQ*qzi>+nw)`Lri?x z8da><+gQDDjZnE)16WmoD?hGm0ATAkEa_u9-Uu<%lD?WkNo{Ri-b~k1G$gg;4r~BB z`Ot4|VNqLHYgcZ`z%$VHW%H_7+sjzvcPHMX$dcrR>$3RIA$o_rnGs6?9E2$C^fbghfF1-+5hX;?gYhUP!{e$P z6lG~13_W59>+o3EaFv=V#$XtFiJc~GBsffEIBZ@Yy)iyMd<$c@ZoED*dK>PIak#Yx zUiV+Rbwly`1u_0wkO#BKEn<%$eJENmLn-d-!3f&C1isRFmr2ovB9izaj{h?<5bUef zrx%$6RGPgf;YGK6tyA~sX)z9)Ie7)H+n=jS4&mq-rt$7awQ)tx(0a2_Z+51^9QyWwtpeDJBeYu(+o+EH-(if&)t=EIv*|MKF>V)o|4 z@21IL3AE3fct?3``oQrc$4?&=8V(m54yW~?Cfi&Y*UIUq)_tItLxJ^h(RvtER&&RO zr|JC%kA@x(<%ZWi2lC}#$=mY5n?HIp`_7}cAHSX3pKH&x7rh74mrG7B-FSZns2?sm zp%d1Adi5KBet*4ZDBm-*(X{*V^wXx}>rKbkP86C>6`M}w8%}L_cW0X(z4iDl=wM$* z`bPT3GjB7-+tWAThGKK36U&1ugIU+ZSJJPPcJHNXdZuH;-jMMw`&azgV-ExA0NmfQ zuE(cAyLUIGFQza4ic;pty8FoL#e%!9=*paLY71=rK_dURYT)Q;=g~ALQbxe-|=nD62-|+3KS~JSkUg?R?#F05!>$ zgFQgJB1#aiw3>6&^ia1+OlucGT$b3_nVMR4+fLm$D#l-VOdTDwW-F?D9E8n4KS3mZ z(Re^QAax#)6lT$NK%!^$&;cHD;MZl`)&WI)f+qr2wI|?VP*#0V0Be*3Gxd=GJ$$jL zMlq=5-$Aijfp(w{TAeFrvz_$jnLAnRI0ZM*4Nqg{%}3`RpUa*6aBx-rWTDh?=)<9W z$C*DLUYq*)rB5$yY1xjmUodRr+4N|76u7a`oA(|rc)E(7F3_L+pEdrXwbXO$lQ%!p z{lcC__KC076sy^&WheC_Fiem}Pv;sv9fxN{jcW8^+iJ&6Yk z?%zsF08DfNFjZeVCNIVFZJlZl)t_k+h zs+*PX#g)RieCP&|Vtxf?Ea~xgz{a%2=*-O}oAaryd)?N(dZKvba=~_`XuFcPT`75X zWllcx?9TM>)bUWH2tCiK!kdGPo#7+M`nZz4q>(Ek3IfA|)&)n}$3)Y@N~k5JUh1Qe z+#wd#D*`%;Hiu;25vn(U+I0he2yIelh1J7X*KO@guF_6aJ!ZdLb+X^ZgNcM3irJ0; zXS)V85SZ(>2Nk?S(y?mvNDyt!1U+?PF5q7OUQxTr6+d+Fg6Pzc-Bx1aP6KB&yaIi(S#%k7nGc}^ji1}NG^8Pa#bz& zmU-_TWUC)Tb=VbL-VgTuXkWp#zv$YZHf%W2R&=o7>?%6D;M}!3)92vCXx#nO<6HOm zR__-)14Yk3-ZlWa4tLSsme!O{W%(zI1!s5B*^TG&E35q4$l8gucYfag$-Te+=DP1v z-gikotGn0T-C*Kx`mC|wK3{a7&)d#JA8<5Z%#4E3e=%=r-g)#Ok@4U2K>)wy!dbO5 z)N&4$cv!nTu%G@FbRvF^;I9Exi>tR*64hl~Y_FwjjYz9?zK2xZ2i?IH(`0>WI=F5+ zn3IZ!h6|>VqG=>=8i84+<(+~FCm5aDC`9Tux|)j?h($&4ss*WD5q-jfV;(V=(~|`C zOt^H0_^E*USWrGv{tc8=A1Q<;^;=Ud6RETPKF+oinx~iz=f3Ryoc!U!TIZ)ns4?LS zE!%MXO9lWk!*Mca@t07$YcCo@V!@~Y#t=&&0FNJu2*z!Jz&nXxL@hA}0(?A#*{}!0 z!7vj*lL!$P#Wps4du-rj;NsZut=o#DB46ZXQn5#a4~Q)wfbxdkiSYbJi!9M_tZZnq zU^RLeD^~i2cM{N2|2@*mp%w+^L}5DofN18#>>~aV6pMe1b*#iV6BXcHmlyvGvvD&} zOzK82iGPWC{|W(0rugp=d=5adtA?GB5Qzi=U95+92)&7ScI#fNuwVlITdJP46ZFrw;nV_0AFY!`R-dZ=o=A{#!5}h0BnG* zejA|thNd^%U@HOl4-C)o6{*fd%lLKRtE;b9QT^bJ2Ig9w-vq{4GjD+1-$-5ygRg=z zdog!$Ekb?SYrR85!R0fxl^&+htrV5uFl5d?%-@t@d;Wd<_PXV+SX{ zqrT+&!qXvN`9Tz~e7*#F3QTg8vAJLL_fL;Oc-fqE@ zXi#3513NIF1YQ8LNb}&b7>v?Pl7SU(lsmw>s{etnQhIfo!#8(nP6&mk!vcSli^l|R zJ{*Z~Qvw%E$nhB=CW!F5%5#T_-vum#2ydiOaIJt_ZC6cKRo#H2L^L7E*h^o}u>s-> z5DdX1t#p**;ki1-;gyt3RupE02Xo;15t|XX$;r2Ty?s4rhI{Zo24+enY9_F*jOKhfXU*LSpn_mm*d3*b#~3~~u4ohgT-fIuh$ z%XeQ;*BdGA^C2yT!EKKS%mvFdnI*hKgZUj6QYBJ|IENhN=4W9DTyr`Jj;x^ogyMkG zuq6}B5U_skA`JjoeDXTsVnPDsYG~1wfYWnQ}KADhI-T-yp8tA0;k%| zkE-4^$caJlsZo74hzB_^DeygTi|Y9jhe=RAL$#C$Wk-HSXMl)0cN!1}~F z@a_PKL2sh6iwfN+CMQwqCMT=>eyUnU;<1@p{<#nGf#Qw^qrpM$m~VheyKkFgsp#Q~ z#}lv}g%UuBm>dBIq!<8o`+`z<8&WqQWQa+M56b%eFi?zcO>_fNcNAPTDrI#~MO7R_ z>d;-LzEXv}Sf{*G5T2nPSL#j%{2mU?a&%XT0LH326&Di#X^=u#TsfWqRfDf0O{LH2 zNN@(26S@Qq1N@>udXVi1gHXnwjVB^N8<2_@N>vS-MD~a)2r@Xfhyy!O1KQcjBnV;A ztA`325Ej`H#h8cy<%}V+chvM=W?X_Z2y=J_9$RqT5aao1*F5`}jXVUQA%BhSY< zK0G}w;L6r?S;kdDGJ(b<3F3X|F-Y1iAUopLRB_$H#~tuPTg2JHpi#<2i`vfqP7&|A z;Zm0gUrBU$6<>S>vk99I;g;8lnVmPIH-|?ree?R2)34pQ+W*F=6a>$zzSm9+EWCQ+ zPDq@YiU+4+GXqoirg|s3EaHEFUd6vf@OKFQBLGE*nr4;B=ut%# zinlR_PY?vWhZ^vCNR>6S6oU$o2{j@UzUA9mQlsE>kRDD8%VR5J@Rr}$l)C=R+lq=9 z>mE#9!#{c-fNm;5tHz$6gUDA?Q{XMx2&#&ScsKp;RTXq)r2omMtc4ICH7cH8t1492 z`C3)sL86I6=k~^5t*Q+44}7hv;Gyufs`9m}Qsve1wW^GZOqa z+2Mis_1MB2!U%tB^ufu|*L}yvP6$`P8-HMKM!GRG8kXJ|xpK8Xbn1>jn7G|H(Pe#+ zRwHfz9(76$v1p0^6$^+(s!p9jyE)Mk3aHLpgwzYwnQy;Loq-nzv_cV`M(aLYrPFk= zm%9!UWlNlaZWUuBJ~IPKpJJX7BaWG*NZuWL#Td`6=l;!U-klpWa>_`d)!u3{=@h#?#j?2vGW z3IxO{>>!5V9)d7}hY0=@0kP$O0x`u2m%HomKQB~6K6r6K#x0`1Dn^LjhMSk-xPs=3 zaA*vaarjTy)ZRz*O0pWYEA$)FIcg+5Fa9?e0al5T{s|zwu(523c_q)h0%kqdnR4J? zi6NhqBj0o=GgE9jv}!6e9V<2+%V@U@EPE2nv0nu|V>ZG*OAn)*T6FQqR$9Co!isL z(r+%GTRE39m8>>M++H478GwY@l6Iv}EH|w*LGQY84GR{<7sJePaGTAEA12psed;fH xyhTsv7h0_PWpx8ycZMC@Vs?Vexi2vH%k833%@7NY0XqR!h1@S+P(&^X{|i3oXfXf) literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/sun_md5_crypt.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/sun_md5_crypt.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c476ed96105068ec76fb8808681364babc37882c GIT binary patch literal 13505 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 literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/windows.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/handlers/__pycache__/windows.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f8e014486c614d46e2a087aa351d96c1e9ae52f6 GIT binary patch literal 12429 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 literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/handlers/argon2.py b/ansible/lib/python3.11/site-packages/passlib/handlers/argon2.py new file mode 100644 index 000000000..4a5691b45 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/handlers/argon2.py @@ -0,0 +1,1009 @@ +"""passlib.handlers.argon2 -- argon2 password hash wrapper + +References +========== +* argon2 + - home: https://github.com/P-H-C/phc-winner-argon2 + - whitepaper: https://github.com/P-H-C/phc-winner-argon2/blob/master/argon2-specs.pdf +* argon2 cffi wrapper + - pypi: https://pypi.python.org/pypi/argon2_cffi + - home: https://github.com/hynek/argon2_cffi +* argon2 pure python + - pypi: https://pypi.python.org/pypi/argon2pure + - home: https://github.com/bwesterb/argon2pure +""" +#============================================================================= +# imports +#============================================================================= +from __future__ import with_statement, absolute_import +# core +import logging +log = logging.getLogger(__name__) +import re +import types +from warnings import warn +# site +_argon2_cffi = None # loaded below +_argon2pure = None # dynamically imported by _load_backend_argon2pure() +# pkg +from passlib import exc +from passlib.crypto.digest import MAX_UINT32 +from passlib.utils import classproperty, to_bytes, render_bytes +from passlib.utils.binary import b64s_encode, b64s_decode +from passlib.utils.compat import u, unicode, bascii_to_str, uascii_to_str, PY2 +import passlib.utils.handlers as uh +# local +__all__ = [ + "argon2", +] + +#============================================================================= +# helpers +#============================================================================= + +# NOTE: when adding a new argon2 hash type, need to do the following: +# * add TYPE_XXX constant, and add to ALL_TYPES +# * make sure "_backend_type_map" constructors handle it correctly for all backends +# * make sure _hash_regex & _ident_regex (below) support type string. +# * add reference vectors for testing. + +#: argon2 type constants -- subclasses handle mapping these to backend-specific type constants. +#: (should be lowercase, to match representation in hash string) +TYPE_I = u("i") +TYPE_D = u("d") +TYPE_ID = u("id") # new 2016-10-29; passlib 1.7.2 requires backends new enough for support + +#: list of all known types; first (supported) type will be used as default. +ALL_TYPES = (TYPE_ID, TYPE_I, TYPE_D) +ALL_TYPES_SET = set(ALL_TYPES) + +#============================================================================= +# import argon2 package (https://pypi.python.org/pypi/argon2_cffi) +#============================================================================= + +# import cffi package +# NOTE: we try to do this even if caller is going to use argon2pure, +# so that we can always use the libargon2 default settings when possible. +_argon2_cffi_error = None +try: + import argon2 as _argon2_cffi +except ImportError: + _argon2_cffi = None +else: + if not hasattr(_argon2_cffi, "Type"): + # they have incompatible "argon2" package installed, instead of "argon2_cffi" package. + _argon2_cffi_error = ( + "'argon2' module points to unsupported 'argon2' pypi package; " + "please install 'argon2-cffi' instead." + ) + _argon2_cffi = None + elif not hasattr(_argon2_cffi, "low_level"): + # they have pre-v16 argon2_cffi package + _argon2_cffi_error = "'argon2-cffi' is too old, please update to argon2_cffi >= 18.2.0" + _argon2_cffi = None + +# init default settings for our hasher class -- +# if we have argon2_cffi >= 16.0, use their default hasher settings, otherwise use static default +if hasattr(_argon2_cffi, "PasswordHasher"): + # use cffi's default settings + _default_settings = _argon2_cffi.PasswordHasher() + _default_version = _argon2_cffi.low_level.ARGON2_VERSION +else: + # use fallback settings (for no backend, or argon2pure) + class _DummyCffiHasher: + """ + dummy object to use as source of defaults when argon2_cffi isn't present. + this tries to mimic the attributes of ``argon2.PasswordHasher()`` which the rest of + this module reads. + + .. note:: values last synced w/ argon2 19.2 as of 2019-11-09 + """ + time_cost = 2 + memory_cost = 512 + parallelism = 2 + salt_len = 16 + hash_len = 16 + # NOTE: "type" attribute added in argon2_cffi v18.2; but currently not reading it + # type = _argon2_cffi.Type.ID + + _default_settings = _DummyCffiHasher() + _default_version = 0x13 # v1.9 + +#============================================================================= +# handler +#============================================================================= +class _Argon2Common(uh.SubclassBackendMixin, uh.ParallelismMixin, + uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, + uh.GenericHandler): + """ + Base class which implements brunt of Argon2 code. + This is then subclassed by the various backends, + to override w/ backend-specific methods. + + When a backend is loaded, the bases of the 'argon2' class proper + are modified to prepend the correct backend-specific subclass. + """ + #=================================================================== + # class attrs + #=================================================================== + + #------------------------ + # PasswordHash + #------------------------ + + name = "argon2" + setting_kwds = ("salt", + "salt_size", + "salt_len", # 'salt_size' alias for compat w/ argon2 package + "rounds", + "time_cost", # 'rounds' alias for compat w/ argon2 package + "memory_cost", + "parallelism", + "digest_size", + "hash_len", # 'digest_size' alias for compat w/ argon2 package + "type", # the type of argon2 hash used + ) + + # TODO: could support the optional 'data' parameter, + # but need to research the uses, what a more descriptive name would be, + # and deal w/ fact that argon2_cffi 16.1 doesn't currently support it. + # (argon2_pure does though) + + #------------------------ + # GenericHandler + #------------------------ + + # NOTE: ident -- all argon2 hashes start with "$argon2$" + # XXX: could programmaticaly generate "ident_values" string from ALL_TYPES above + + checksum_size = _default_settings.hash_len + + #: force parsing these kwds + _always_parse_settings = uh.GenericHandler._always_parse_settings + \ + ("type",) + + #: exclude these kwds from parsehash() result (most are aliases for other keys) + _unparsed_settings = uh.GenericHandler._unparsed_settings + \ + ("salt_len", "time_cost", "hash_len", "digest_size") + + #------------------------ + # HasSalt + #------------------------ + default_salt_size = _default_settings.salt_len + min_salt_size = 8 + max_salt_size = MAX_UINT32 + + #------------------------ + # HasRounds + # TODO: once rounds limit logic is factored out, + # make 'rounds' and 'cost' an alias for 'time_cost' + #------------------------ + default_rounds = _default_settings.time_cost + min_rounds = 1 + max_rounds = MAX_UINT32 + rounds_cost = "linear" + + #------------------------ + # ParalleismMixin + #------------------------ + max_parallelism = (1 << 24) - 1 # from argon2.h / ARGON2_MAX_LANES + + #------------------------ + # custom + #------------------------ + + #: max version support + #: NOTE: this is dependant on the backend, and initialized/modified by set_backend() + max_version = _default_version + + #: minimum version before needs_update() marks the hash; if None, defaults to max_version + min_desired_version = None + + #: minimum valid memory_cost + min_memory_cost = 8 # from argon2.h / ARGON2_MIN_MEMORY + + #: maximum number of threads (-1=unlimited); + #: number of threads used by .hash() will be min(parallelism, max_threads) + max_threads = -1 + + #: global flag signalling argon2pure backend to use threads + #: rather than subprocesses. + pure_use_threads = False + + #: internal helper used to store mapping of TYPE_XXX constants -> backend-specific type constants; + #: this is populated by _load_backend_mixin(); and used to detect which types are supported. + #: XXX: could expose keys as class-level .supported_types property? + _backend_type_map = {} + + @classproperty + def type_values(cls): + """ + return tuple of types supported by this backend + + .. versionadded:: 1.7.2 + """ + cls.get_backend() # make sure backend is loaded + return tuple(cls._backend_type_map) + + #=================================================================== + # instance attrs + #=================================================================== + + #: argon2 hash type, one of ALL_TYPES -- class value controls the default + #: .. versionadded:: 1.7.2 + type = TYPE_ID + + #: parallelism setting -- class value controls the default + parallelism = _default_settings.parallelism + + #: hash version (int) + #: NOTE: this is modified by set_backend() + version = _default_version + + #: memory cost -- class value controls the default + memory_cost = _default_settings.memory_cost + + @property + def type_d(self): + """ + flag indicating a Type D hash + + .. deprecated:: 1.7.2; will be removed in passlib 2.0 + """ + return self.type == TYPE_D + + #: optional secret data + data = None + + #=================================================================== + # variant constructor + #=================================================================== + + @classmethod + def using(cls, type=None, memory_cost=None, salt_len=None, time_cost=None, digest_size=None, + checksum_size=None, hash_len=None, max_threads=None, **kwds): + # support aliases which match argon2 naming convention + if time_cost is not None: + if "rounds" in kwds: + raise TypeError("'time_cost' and 'rounds' are mutually exclusive") + kwds['rounds'] = time_cost + + if salt_len is not None: + if "salt_size" in kwds: + raise TypeError("'salt_len' and 'salt_size' are mutually exclusive") + kwds['salt_size'] = salt_len + + if hash_len is not None: + if digest_size is not None: + raise TypeError("'hash_len' and 'digest_size' are mutually exclusive") + digest_size = hash_len + + if checksum_size is not None: + if digest_size is not None: + raise TypeError("'checksum_size' and 'digest_size' are mutually exclusive") + digest_size = checksum_size + + # create variant + subcls = super(_Argon2Common, cls).using(**kwds) + + # set type + if type is not None: + subcls.type = subcls._norm_type(type) + + # set checksum size + relaxed = kwds.get("relaxed") + if digest_size is not None: + if isinstance(digest_size, uh.native_string_types): + digest_size = int(digest_size) + # NOTE: this isn't *really* digest size minimum, but want to enforce secure minimum. + subcls.checksum_size = uh.norm_integer(subcls, digest_size, min=16, max=MAX_UINT32, + param="digest_size", relaxed=relaxed) + + # set memory cost + if memory_cost is not None: + if isinstance(memory_cost, uh.native_string_types): + memory_cost = int(memory_cost) + subcls.memory_cost = subcls._norm_memory_cost(memory_cost, relaxed=relaxed) + + # validate constraints + subcls._validate_constraints(subcls.memory_cost, subcls.parallelism) + + # set max threads + if max_threads is not None: + if isinstance(max_threads, uh.native_string_types): + max_threads = int(max_threads) + if max_threads < 1 and max_threads != -1: + raise ValueError("max_threads (%d) must be -1 (unlimited), or at least 1." % + (max_threads,)) + subcls.max_threads = max_threads + + return subcls + + @classmethod + def _validate_constraints(cls, memory_cost, parallelism): + # NOTE: this is used by class & instance, hence passing in via arguments. + # could switch and make this a hybrid method. + min_memory_cost = 8 * parallelism + if memory_cost < min_memory_cost: + raise ValueError("%s: memory_cost (%d) is too low, must be at least " + "8 * parallelism (8 * %d = %d)" % + (cls.name, memory_cost, + parallelism, min_memory_cost)) + + #=================================================================== + # public api + #=================================================================== + + #: shorter version of _hash_regex, used to quickly identify hashes + _ident_regex = re.compile(r"^\$argon2[a-z]+\$") + + @classmethod + def identify(cls, hash): + hash = uh.to_unicode_for_identify(hash) + return cls._ident_regex.match(hash) is not None + + # hash(), verify(), genhash() -- implemented by backend subclass + + #=================================================================== + # hash parsing / rendering + #=================================================================== + + # info taken from source of decode_string() function in + # + # + # hash format: + # $argon2[$v=]$m=,t=,p=[,keyid=][,data=][$[$]] + # + # NOTE: as of 2016-6-17, the official source (above) lists the "keyid" param in the comments, + # but the actual source of decode_string & encode_string don't mention it at all. + # we're supporting parsing it, but throw NotImplementedError if encountered. + # + # sample hashes: + # v1.0: '$argon2i$m=512,t=2,p=2$5VtWOO3cGWYQHEMaYGbsfQ$AcmqasQgW/wI6wAHAMk4aQ' + # v1.3: '$argon2i$v=19$m=512,t=2,p=2$5VtWOO3cGWYQHEMaYGbsfQ$AcmqasQgW/wI6wAHAMk4aQ' + + #: regex to parse argon hash + _hash_regex = re.compile(br""" + ^ + \$argon2(?P[a-z]+)\$ + (?: + v=(?P\d+) + \$ + )? + m=(?P\d+) + , + t=(?P\d+) + , + p=(?P\d+) + (?: + ,keyid=(?P[^,$]+) + )? + (?: + ,data=(?P[^,$]+) + )? + (?: + \$ + (?P[^$]+) + (?: + \$ + (?P.+) + )? + )? + $ + """, re.X) + + @classmethod + def from_string(cls, hash): + # NOTE: assuming hash will be unicode, or use ascii-compatible encoding. + # TODO: switch to working w/ str or unicode + if isinstance(hash, unicode): + hash = hash.encode("utf-8") + if not isinstance(hash, bytes): + raise exc.ExpectedStringError(hash, "hash") + m = cls._hash_regex.match(hash) + if not m: + raise exc.MalformedHashError(cls) + type, version, memory_cost, time_cost, parallelism, keyid, data, salt, digest = \ + m.group("type", "version", "memory_cost", "time_cost", "parallelism", + "keyid", "data", "salt", "digest") + if keyid: + raise NotImplementedError("argon2 'keyid' parameter not supported") + return cls( + type=type.decode("ascii"), + version=int(version) if version else 0x10, + memory_cost=int(memory_cost), + rounds=int(time_cost), + parallelism=int(parallelism), + salt=b64s_decode(salt) if salt else None, + data=b64s_decode(data) if data else None, + checksum=b64s_decode(digest) if digest else None, + ) + + def to_string(self): + version = self.version + if version == 0x10: + vstr = "" + else: + vstr = "v=%d$" % version + + data = self.data + if data: + kdstr = ",data=" + bascii_to_str(b64s_encode(self.data)) + else: + kdstr = "" + + # NOTE: 'keyid' param currently not supported + return "$argon2%s$%sm=%d,t=%d,p=%d%s$%s$%s" % ( + uascii_to_str(self.type), + vstr, + self.memory_cost, + self.rounds, + self.parallelism, + kdstr, + bascii_to_str(b64s_encode(self.salt)), + bascii_to_str(b64s_encode(self.checksum)), + ) + + #=================================================================== + # init + #=================================================================== + def __init__(self, type=None, type_d=False, version=None, memory_cost=None, data=None, **kwds): + + # handle deprecated kwds + if type_d: + warn('argon2 `type_d=True` keyword is deprecated, and will be removed in passlib 2.0; ' + 'please use ``type="d"`` instead') + assert type is None + type = TYPE_D + + # TODO: factor out variable checksum size support into a mixin. + # set checksum size to specific value before _norm_checksum() is called + checksum = kwds.get("checksum") + if checksum is not None: + self.checksum_size = len(checksum) + + # call parent + super(_Argon2Common, self).__init__(**kwds) + + # init type + if type is None: + assert uh.validate_default_value(self, self.type, self._norm_type, param="type") + else: + self.type = self._norm_type(type) + + # init version + if version is None: + assert uh.validate_default_value(self, self.version, self._norm_version, + param="version") + else: + self.version = self._norm_version(version) + + # init memory cost + if memory_cost is None: + assert uh.validate_default_value(self, self.memory_cost, self._norm_memory_cost, + param="memory_cost") + else: + self.memory_cost = self._norm_memory_cost(memory_cost) + + # init data + if data is None: + assert self.data is None + else: + if not isinstance(data, bytes): + raise uh.exc.ExpectedTypeError(data, "bytes", "data") + self.data = data + + #------------------------------------------------------------------- + # parameter guards + #------------------------------------------------------------------- + + @classmethod + def _norm_type(cls, value): + # type check + if not isinstance(value, unicode): + if PY2 and isinstance(value, bytes): + value = value.decode('ascii') + else: + raise uh.exc.ExpectedTypeError(value, "str", "type") + + # check if type is valid + if value in ALL_TYPES_SET: + return value + + # translate from uppercase + temp = value.lower() + if temp in ALL_TYPES_SET: + return temp + + # failure! + raise ValueError("unknown argon2 hash type: %r" % (value,)) + + @classmethod + def _norm_version(cls, version): + if not isinstance(version, uh.int_types): + raise uh.exc.ExpectedTypeError(version, "integer", "version") + + # minimum valid version + if version < 0x13 and version != 0x10: + raise ValueError("invalid argon2 hash version: %d" % (version,)) + + # check this isn't past backend's max version + backend = cls.get_backend() + if version > cls.max_version: + raise ValueError("%s: hash version 0x%X not supported by %r backend " + "(max version is 0x%X); try updating or switching backends" % + (cls.name, version, backend, cls.max_version)) + return version + + @classmethod + def _norm_memory_cost(cls, memory_cost, relaxed=False): + return uh.norm_integer(cls, memory_cost, min=cls.min_memory_cost, + param="memory_cost", relaxed=relaxed) + + #=================================================================== + # digest calculation + #=================================================================== + + # NOTE: _calc_checksum implemented by backend subclass + + @classmethod + def _get_backend_type(cls, value): + """ + helper to resolve backend constant from type + """ + try: + return cls._backend_type_map[value] + except KeyError: + pass + # XXX: pick better error class? + msg = "unsupported argon2 hash (type %r not supported by %s backend)" % \ + (value, cls.get_backend()) + raise ValueError(msg) + + #=================================================================== + # hash migration + #=================================================================== + + def _calc_needs_update(self, **kwds): + cls = type(self) + if self.type != cls.type: + return True + minver = cls.min_desired_version + if minver is None or minver > cls.max_version: + minver = cls.max_version + if self.version < minver: + # version is too old. + return True + if self.memory_cost != cls.memory_cost: + return True + if self.checksum_size != cls.checksum_size: + return True + return super(_Argon2Common, self)._calc_needs_update(**kwds) + + #=================================================================== + # backend loading + #=================================================================== + + _no_backend_suggestion = " -- recommend you install one (e.g. 'pip install argon2_cffi')" + + @classmethod + def _finalize_backend_mixin(mixin_cls, name, dryrun): + """ + helper called by from backend mixin classes' _load_backend_mixin() -- + invoked after backend imports have been loaded, and performs + feature detection & testing common to all backends. + """ + # check argon2 version + max_version = mixin_cls.max_version + assert isinstance(max_version, int) and max_version >= 0x10 + if max_version < 0x13: + warn("%r doesn't support argon2 v1.3, and should be upgraded" % name, + uh.exc.PasslibSecurityWarning) + + # prefer best available type + for type in ALL_TYPES: + if type in mixin_cls._backend_type_map: + mixin_cls.type = type + break + else: + warn("%r lacks support for all known hash types" % name, uh.exc.PasslibRuntimeWarning) + # NOTE: class will just throw "unsupported argon2 hash" error if they try to use it... + mixin_cls.type = TYPE_ID + + return True + + @classmethod + def _adapt_backend_error(cls, err, hash=None, self=None): + """ + internal helper invoked when backend has hash/verification error; + used to adapt to passlib message. + """ + backend = cls.get_backend() + + # parse hash to throw error if format was invalid, parameter out of range, etc. + if self is None and hash is not None: + self = cls.from_string(hash) + + # check constraints on parsed object + # XXX: could move this to __init__, but not needed by needs_update calls + if self is not None: + self._validate_constraints(self.memory_cost, self.parallelism) + + # as of cffi 16.1, lacks support in hash_secret(), so genhash() will get here. + # as of cffi 16.2, support removed from verify_secret() as well. + if backend == "argon2_cffi" and self.data is not None: + raise NotImplementedError("argon2_cffi backend doesn't support the 'data' parameter") + + # fallback to reporting a malformed hash + text = str(err) + if text not in [ + "Decoding failed" # argon2_cffi's default message + ]: + reason = "%s reported: %s: hash=%r" % (backend, text, hash) + else: + reason = repr(hash) + raise exc.MalformedHashError(cls, reason=reason) + + #=================================================================== + # eoc + #=================================================================== + +#----------------------------------------------------------------------- +# stub backend +#----------------------------------------------------------------------- +class _NoBackend(_Argon2Common): + """ + mixin used before any backend has been loaded. + contains stubs that force loading of one of the available backends. + """ + #=================================================================== + # primary methods + #=================================================================== + @classmethod + def hash(cls, secret): + cls._stub_requires_backend() + return cls.hash(secret) + + @classmethod + def verify(cls, secret, hash): + cls._stub_requires_backend() + return cls.verify(secret, hash) + + @uh.deprecated_method(deprecated="1.7", removed="2.0") + @classmethod + def genhash(cls, secret, config): + cls._stub_requires_backend() + return cls.genhash(secret, config) + + #=================================================================== + # digest calculation + #=================================================================== + def _calc_checksum(self, secret): + # NOTE: since argon2_cffi takes care of rendering hash, + # _calc_checksum() is only used by the argon2pure backend. + self._stub_requires_backend() + # NOTE: have to use super() here so that we don't recursively + # call subclass's wrapped _calc_checksum + return super(argon2, self)._calc_checksum(secret) + + #=================================================================== + # eoc + #=================================================================== + +#----------------------------------------------------------------------- +# argon2_cffi backend +#----------------------------------------------------------------------- +class _CffiBackend(_Argon2Common): + """ + argon2_cffi backend + """ + #=================================================================== + # backend loading + #=================================================================== + + @classmethod + def _load_backend_mixin(mixin_cls, name, dryrun): + # make sure we write info to base class's __dict__, not that of a subclass + assert mixin_cls is _CffiBackend + + # we automatically import this at top, so just grab info + if _argon2_cffi is None: + if _argon2_cffi_error: + raise exc.PasslibSecurityError(_argon2_cffi_error) + return False + max_version = _argon2_cffi.low_level.ARGON2_VERSION + log.debug("detected 'argon2_cffi' backend, version %r, with support for 0x%x argon2 hashes", + _argon2_cffi.__version__, max_version) + + # build type map + TypeEnum = _argon2_cffi.Type + type_map = {} + for type in ALL_TYPES: + try: + type_map[type] = getattr(TypeEnum, type.upper()) + except AttributeError: + # TYPE_ID support not added until v18.2 + assert type not in (TYPE_I, TYPE_D), "unexpected missing type: %r" % type + mixin_cls._backend_type_map = type_map + + # set version info, and run common setup + mixin_cls.version = mixin_cls.max_version = max_version + return mixin_cls._finalize_backend_mixin(name, dryrun) + + #=================================================================== + # primary methods + #=================================================================== + @classmethod + def hash(cls, secret): + # TODO: add in 'encoding' support once that's finalized in 1.8 / 1.9. + uh.validate_secret(secret) + secret = to_bytes(secret, "utf-8") + # XXX: doesn't seem to be a way to make this honor max_threads + try: + return bascii_to_str(_argon2_cffi.low_level.hash_secret( + type=cls._get_backend_type(cls.type), + memory_cost=cls.memory_cost, + time_cost=cls.default_rounds, + parallelism=cls.parallelism, + salt=to_bytes(cls._generate_salt()), + hash_len=cls.checksum_size, + secret=secret, + )) + except _argon2_cffi.exceptions.HashingError as err: + raise cls._adapt_backend_error(err) + + #: helper for verify() method below -- maps prefixes to type constants + _byte_ident_map = dict((render_bytes(b"$argon2%s$", type.encode("ascii")), type) + for type in ALL_TYPES) + + @classmethod + def verify(cls, secret, hash): + # TODO: add in 'encoding' support once that's finalized in 1.8 / 1.9. + uh.validate_secret(secret) + secret = to_bytes(secret, "utf-8") + hash = to_bytes(hash, "ascii") + + # read type from start of hash + # NOTE: don't care about malformed strings, lowlevel will throw error for us + type = cls._byte_ident_map.get(hash[:1+hash.find(b"$", 1)], TYPE_I) + type_code = cls._get_backend_type(type) + + # XXX: doesn't seem to be a way to make this honor max_threads + try: + result = _argon2_cffi.low_level.verify_secret(hash, secret, type_code) + assert result is True + return True + except _argon2_cffi.exceptions.VerifyMismatchError: + return False + except _argon2_cffi.exceptions.VerificationError as err: + raise cls._adapt_backend_error(err, hash=hash) + + # NOTE: deprecated, will be removed in 2.0 + @classmethod + def genhash(cls, secret, config): + # TODO: add in 'encoding' support once that's finalized in 1.8 / 1.9. + uh.validate_secret(secret) + secret = to_bytes(secret, "utf-8") + self = cls.from_string(config) + # XXX: doesn't seem to be a way to make this honor max_threads + try: + result = bascii_to_str(_argon2_cffi.low_level.hash_secret( + type=cls._get_backend_type(self.type), + memory_cost=self.memory_cost, + time_cost=self.rounds, + parallelism=self.parallelism, + salt=to_bytes(self.salt), + hash_len=self.checksum_size, + secret=secret, + version=self.version, + )) + except _argon2_cffi.exceptions.HashingError as err: + raise cls._adapt_backend_error(err, hash=config) + if self.version == 0x10: + # workaround: argon2 0x13 always returns "v=" segment, even for 0x10 hashes + result = result.replace("$v=16$", "$") + return result + + #=================================================================== + # digest calculation + #=================================================================== + def _calc_checksum(self, secret): + raise AssertionError("shouldn't be called under argon2_cffi backend") + + #=================================================================== + # eoc + #=================================================================== + +#----------------------------------------------------------------------- +# argon2pure backend +#----------------------------------------------------------------------- +class _PureBackend(_Argon2Common): + """ + argon2pure backend + """ + #=================================================================== + # backend loading + #=================================================================== + + @classmethod + def _load_backend_mixin(mixin_cls, name, dryrun): + # make sure we write info to base class's __dict__, not that of a subclass + assert mixin_cls is _PureBackend + + # import argon2pure + global _argon2pure + try: + import argon2pure as _argon2pure + except ImportError: + return False + + # get default / max supported version -- added in v1.2.2 + try: + from argon2pure import ARGON2_DEFAULT_VERSION as max_version + except ImportError: + log.warning("detected 'argon2pure' backend, but package is too old " + "(passlib requires argon2pure >= 1.2.3)") + return False + + log.debug("detected 'argon2pure' backend, with support for 0x%x argon2 hashes", + max_version) + + if not dryrun: + warn("Using argon2pure backend, which is 100x+ slower than is required " + "for adequate security. Installing argon2_cffi (via 'pip install argon2_cffi') " + "is strongly recommended", exc.PasslibSecurityWarning) + + # build type map + type_map = {} + for type in ALL_TYPES: + try: + type_map[type] = getattr(_argon2pure, "ARGON2" + type.upper()) + except AttributeError: + # TYPE_ID support not added until v1.3 + assert type not in (TYPE_I, TYPE_D), "unexpected missing type: %r" % type + mixin_cls._backend_type_map = type_map + + mixin_cls.version = mixin_cls.max_version = max_version + return mixin_cls._finalize_backend_mixin(name, dryrun) + + #=================================================================== + # primary methods + #=================================================================== + + # NOTE: this backend uses default .hash() & .verify() implementations. + + #=================================================================== + # digest calculation + #=================================================================== + def _calc_checksum(self, secret): + # TODO: add in 'encoding' support once that's finalized in 1.8 / 1.9. + uh.validate_secret(secret) + secret = to_bytes(secret, "utf-8") + kwds = dict( + password=secret, + salt=self.salt, + time_cost=self.rounds, + memory_cost=self.memory_cost, + parallelism=self.parallelism, + tag_length=self.checksum_size, + type_code=self._get_backend_type(self.type), + version=self.version, + ) + if self.max_threads > 0: + kwds['threads'] = self.max_threads + if self.pure_use_threads: + kwds['use_threads'] = True + if self.data: + kwds['associated_data'] = self.data + # NOTE: should return raw bytes + # NOTE: this may raise _argon2pure.Argon2ParameterError, + # but it if does that, there's a bug in our own parameter checking code. + try: + return _argon2pure.argon2(**kwds) + except _argon2pure.Argon2Error as err: + raise self._adapt_backend_error(err, self=self) + + #=================================================================== + # eoc + #=================================================================== + +class argon2(_NoBackend, _Argon2Common): + """ + This class implements the Argon2 password hash [#argon2-home]_, and follows the :ref:`password-hash-api`. + + Argon2 supports a variable-length salt, and variable time & memory cost, + and a number of other configurable parameters. + + The :meth:`~passlib.ifc.PasswordHash.replace` method accepts the following optional keywords: + + :type type: str + :param type: + Specify the type of argon2 hash to generate. + Can be one of "ID", "I", "D". + + This defaults to "ID" if supported by the backend, otherwise "I". + + :type salt: str + :param salt: + Optional salt string. + If specified, the length must be between 0-1024 bytes. + If not specified, one will be auto-generated (this is recommended). + + :type salt_size: int + :param salt_size: + Optional number of bytes to use when autogenerating new salts. + + :type rounds: int + :param rounds: + Optional number of rounds to use. + This corresponds linearly to the amount of time hashing will take. + + :type time_cost: int + :param time_cost: + An alias for **rounds**, for compatibility with underlying argon2 library. + + :param int memory_cost: + Defines the memory usage in kibibytes. + This corresponds linearly to the amount of memory hashing will take. + + :param int parallelism: + Defines the parallelization factor. + *NOTE: this will affect the resulting hash value.* + + :param int digest_size: + Length of the digest in bytes. + + :param int max_threads: + Maximum number of threads that will be used. + -1 means unlimited; otherwise hashing will use ``min(parallelism, max_threads)`` threads. + + .. note:: + + This option is currently only honored by the argon2pure backend. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include ``rounds`` + that are too small or too large, and ``salt`` strings that are too long. + + .. versionchanged:: 1.7.2 + + Added the "type" keyword, and support for type "D" and "ID" hashes. + (Prior versions could verify type "D" hashes, but not generate them). + + .. todo:: + + * Support configurable threading limits. + """ + #============================================================================= + # backend + #============================================================================= + + # NOTE: the brunt of the argon2 class is implemented in _Argon2Common. + # there are then subclass for each backend (e.g. _PureBackend), + # these are dynamically prepended to this class's bases + # in order to load the appropriate backend. + + #: list of potential backends + backends = ("argon2_cffi", "argon2pure") + + #: flag that this class's bases should be modified by SubclassBackendMixin + _backend_mixin_target = True + + #: map of backend -> mixin class, used by _get_backend_loader() + _backend_mixin_map = { + None: _NoBackend, + "argon2_cffi": _CffiBackend, + "argon2pure": _PureBackend, + } + + #============================================================================= + # + #============================================================================= + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/handlers/bcrypt.py b/ansible/lib/python3.11/site-packages/passlib/handlers/bcrypt.py new file mode 100644 index 000000000..b83b1107e --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/handlers/bcrypt.py @@ -0,0 +1,1243 @@ +"""passlib.bcrypt -- implementation of OpenBSD's BCrypt algorithm. + +TODO: + +* support 2x and altered-2a hashes? + http://www.openwall.com/lists/oss-security/2011/06/27/9 + +* deal with lack of PY3-compatibile c-ext implementation +""" +#============================================================================= +# imports +#============================================================================= +from __future__ import with_statement, absolute_import +# core +from base64 import b64encode +from hashlib import sha256 +import os +import re +import logging; log = logging.getLogger(__name__) +from warnings import warn +# site +_bcrypt = None # dynamically imported by _load_backend_bcrypt() +_pybcrypt = None # dynamically imported by _load_backend_pybcrypt() +_bcryptor = None # dynamically imported by _load_backend_bcryptor() +# pkg +_builtin_bcrypt = None # dynamically imported by _load_backend_builtin() +from passlib.crypto.digest import compile_hmac +from passlib.exc import PasslibHashWarning, PasslibSecurityWarning, PasslibSecurityError +from passlib.utils import safe_crypt, repeat_string, to_bytes, parse_version, \ + rng, getrandstr, test_crypt, to_unicode, \ + utf8_truncate, utf8_repeat_string, crypt_accepts_bytes +from passlib.utils.binary import bcrypt64 +from passlib.utils.compat import get_unbound_method_function +from passlib.utils.compat import u, uascii_to_str, unicode, str_to_uascii, PY3, error_from +import passlib.utils.handlers as uh + +# local +__all__ = [ + "bcrypt", +] + +#============================================================================= +# support funcs & constants +#============================================================================= +IDENT_2 = u("$2$") +IDENT_2A = u("$2a$") +IDENT_2X = u("$2x$") +IDENT_2Y = u("$2y$") +IDENT_2B = u("$2b$") +_BNULL = b'\x00' + +# reference hash of "test", used in various self-checks +TEST_HASH_2A = "$2a$04$5BJqKfqMQvV7nS.yUguNcueVirQqDBGaLXSqj.rs.pZPlNR0UX/HK" + +def _detect_pybcrypt(): + """ + internal helper which tries to distinguish pybcrypt vs bcrypt. + + :returns: + True if cext-based py-bcrypt, + False if ffi-based bcrypt, + None if 'bcrypt' module not found. + + .. versionchanged:: 1.6.3 + + Now assuming bcrypt installed, unless py-bcrypt explicitly detected. + Previous releases assumed py-bcrypt by default. + + Making this change since py-bcrypt is (apparently) unmaintained and static, + whereas bcrypt is being actively maintained, and it's internal structure may shift. + """ + # NOTE: this is also used by the unittests. + + # check for module. + try: + import bcrypt + except ImportError: + # XXX: this is ignoring case where py-bcrypt's "bcrypt._bcrypt" C Ext fails to import; + # would need to inspect actual ImportError message to catch that. + return None + + # py-bcrypt has a "._bcrypt.__version__" attribute (confirmed for v0.1 - 0.4), + # which bcrypt lacks (confirmed for v1.0 - 2.0) + # "._bcrypt" alone isn't sufficient, since bcrypt 2.0 now has that attribute. + try: + from bcrypt._bcrypt import __version__ + except ImportError: + return False + return True + +#============================================================================= +# backend mixins +#============================================================================= +class _BcryptCommon(uh.SubclassBackendMixin, uh.TruncateMixin, uh.HasManyIdents, + uh.HasRounds, uh.HasSalt, uh.GenericHandler): + """ + Base class which implements brunt of BCrypt code. + This is then subclassed by the various backends, + to override w/ backend-specific methods. + + When a backend is loaded, the bases of the 'bcrypt' class proper + are modified to prepend the correct backend-specific subclass. + """ + #=================================================================== + # class attrs + #=================================================================== + + #-------------------- + # PasswordHash + #-------------------- + name = "bcrypt" + setting_kwds = ("salt", "rounds", "ident", "truncate_error") + + #-------------------- + # GenericHandler + #-------------------- + checksum_size = 31 + checksum_chars = bcrypt64.charmap + + #-------------------- + # HasManyIdents + #-------------------- + default_ident = IDENT_2B + ident_values = (IDENT_2, IDENT_2A, IDENT_2X, IDENT_2Y, IDENT_2B) + ident_aliases = {u("2"): IDENT_2, u("2a"): IDENT_2A, u("2y"): IDENT_2Y, + u("2b"): IDENT_2B} + + #-------------------- + # HasSalt + #-------------------- + min_salt_size = max_salt_size = 22 + salt_chars = bcrypt64.charmap + + # NOTE: 22nd salt char must be in restricted set of ``final_salt_chars``, not full set above. + final_salt_chars = ".Oeu" # bcrypt64._padinfo2[1] + + #-------------------- + # HasRounds + #-------------------- + default_rounds = 12 # current passlib default + min_rounds = 4 # minimum from bcrypt specification + max_rounds = 31 # 32-bit integer limit (since real_rounds=1< class + + # NOTE: set_backend() will execute the ._load_backend_mixin() + # of the matching mixin class, which will handle backend detection + + # appended to HasManyBackends' "no backends available" error message + _no_backend_suggestion = " -- recommend you install one (e.g. 'pip install bcrypt')" + + @classmethod + def _finalize_backend_mixin(mixin_cls, backend, dryrun): + """ + helper called by from backend mixin classes' _load_backend_mixin() -- + invoked after backend imports have been loaded, and performs + feature detection & testing common to all backends. + """ + #---------------------------------------------------------------- + # setup helpers + #---------------------------------------------------------------- + assert mixin_cls is bcrypt._backend_mixin_map[backend], \ + "_configure_workarounds() invoked from wrong class" + + if mixin_cls._workrounds_initialized: + return True + + verify = mixin_cls.verify + + err_types = (ValueError, uh.exc.MissingBackendError) + if _bcryptor: + err_types += (_bcryptor.engine.SaltError,) + + def safe_verify(secret, hash): + """verify() wrapper which traps 'unknown identifier' errors""" + try: + return verify(secret, hash) + except err_types: + # backends without support for given ident will throw various + # errors about unrecognized version: + # os_crypt -- internal code below throws + # - PasswordValueError if there's encoding issue w/ password. + # - InternalBackendError if crypt fails for unknown reason + # (trapped below so we can debug it) + # pybcrypt, bcrypt -- raises ValueError + # bcryptor -- raises bcryptor.engine.SaltError + return NotImplemented + except uh.exc.InternalBackendError: + # _calc_checksum() code may also throw CryptBackendError + # if correct hash isn't returned (e.g. 2y hash converted to 2b, + # such as happens with bcrypt 3.0.0) + log.debug("trapped unexpected response from %r backend: verify(%r, %r):", + backend, secret, hash, exc_info=True) + return NotImplemented + + def assert_lacks_8bit_bug(ident): + """ + helper to check for cryptblowfish 8bit bug (fixed in 2y/2b); + even though it's not known to be present in any of passlib's backends. + this is treated as FATAL, because it can easily result in seriously malformed hashes, + and we can't correct for it ourselves. + + test cases from + reference hash is the incorrectly generated $2x$ hash taken from above url + """ + # NOTE: passlib 1.7.2 and earlier used the commented-out LATIN-1 test vector to detect + # this bug; but python3's crypt.crypt() only supports unicode inputs (and + # always encodes them as UTF8 before passing to crypt); so passlib 1.7.3 + # switched to the UTF8-compatible test vector below. This one's bug_hash value + # ("$2x$...rcAS") was drawn from the same openwall source (above); and the correct + # hash ("$2a$...X6eu") was generated by passing the raw bytes to python2's + # crypt.crypt() using OpenBSD 6.7 (hash confirmed as same for $2a$ & $2b$). + + # LATIN-1 test vector + # secret = b"\xA3" + # bug_hash = ident.encode("ascii") + b"05$/OK.fbVrR/bpIqNJ5ianF.CE5elHaaO4EbggVDjb8P19RukzXSM3e" + # correct_hash = ident.encode("ascii") + b"05$/OK.fbVrR/bpIqNJ5ianF.Sa7shbm4.OzKpvFnX1pQLmQW96oUlCq" + + # UTF-8 test vector + secret = b"\xd1\x91" # aka "\u0451" + bug_hash = ident.encode("ascii") + b"05$6bNw2HLQYeqHYyBfLMsv/OiwqTymGIGzFsA4hOTWebfehXHNprcAS" + correct_hash = ident.encode("ascii") + b"05$6bNw2HLQYeqHYyBfLMsv/OUcZd0LKP39b87nBw3.S2tVZSqiQX6eu" + + if verify(secret, bug_hash): + # NOTE: this only EVER be observed in (broken) 2a and (backward-compat) 2x hashes + # generated by crypt_blowfish library. 2y/2b hashes should not have the bug + # (but we check w/ them anyways). + raise PasslibSecurityError( + "passlib.hash.bcrypt: Your installation of the %r backend is vulnerable to " + "the crypt_blowfish 8-bit bug (CVE-2011-2483) under %r hashes, " + "and should be upgraded or replaced with another backend" % (backend, ident)) + + # it doesn't have wraparound bug, but make sure it *does* verify against the correct + # hash, or we're in some weird third case! + if not verify(secret, correct_hash): + raise RuntimeError("%s backend failed to verify %s 8bit hash" % (backend, ident)) + + def detect_wrap_bug(ident): + """ + check for bsd wraparound bug (fixed in 2b) + this is treated as a warning, because it's rare in the field, + and pybcrypt (as of 2015-7-21) is unpatched, but some people may be stuck with it. + + test cases from + + NOTE: reference hash is of password "0"*72 + + NOTE: if in future we need to deliberately create hashes which have this bug, + can use something like 'hashpw(repeat_string(secret[:((1+secret) % 256) or 1]), 72)' + """ + # check if it exhibits wraparound bug + secret = (b"0123456789"*26)[:255] + bug_hash = ident.encode("ascii") + b"04$R1lJ2gkNaoPGdafE.H.16.nVyh2niHsGJhayOHLMiXlI45o8/DU.6" + if verify(secret, bug_hash): + return True + + # if it doesn't have wraparound bug, make sure it *does* handle things + # correctly -- or we're in some weird third case. + correct_hash = ident.encode("ascii") + b"04$R1lJ2gkNaoPGdafE.H.16.1MKHPvmKwryeulRe225LKProWYwt9Oi" + if not verify(secret, correct_hash): + raise RuntimeError("%s backend failed to verify %s wraparound hash" % (backend, ident)) + + return False + + def assert_lacks_wrap_bug(ident): + if not detect_wrap_bug(ident): + return + # should only see in 2a, later idents should NEVER exhibit this bug: + # * 2y implementations should have been free of it + # * 2b was what (supposedly) fixed it + raise RuntimeError("%s backend unexpectedly has wraparound bug for %s" % (backend, ident)) + + #---------------------------------------------------------------- + # check for old 20 support + #---------------------------------------------------------------- + test_hash_20 = b"$2$04$5BJqKfqMQvV7nS.yUguNcuRfMMOXK0xPWavM7pOzjEi5ze5T1k8/S" + result = safe_verify("test", test_hash_20) + if result is NotImplemented: + mixin_cls._lacks_20_support = True + log.debug("%r backend lacks $2$ support, enabling workaround", backend) + elif not result: + raise RuntimeError("%s incorrectly rejected $2$ hash" % backend) + + #---------------------------------------------------------------- + # check for 2a support + #---------------------------------------------------------------- + result = safe_verify("test", TEST_HASH_2A) + if result is NotImplemented: + # 2a support is required, and should always be present + raise RuntimeError("%s lacks support for $2a$ hashes" % backend) + elif not result: + raise RuntimeError("%s incorrectly rejected $2a$ hash" % backend) + else: + assert_lacks_8bit_bug(IDENT_2A) + if detect_wrap_bug(IDENT_2A): + if backend == "os_crypt": + # don't make this a warning for os crypt (e.g. openbsd); + # they'll have proper 2b implementation which will be used for new hashes. + # so even if we didn't have a workaround, this bug wouldn't be a concern. + log.debug("%r backend has $2a$ bsd wraparound bug, enabling workaround", backend) + else: + # installed library has the bug -- want to let users know, + # so they can upgrade it to something better (e.g. bcrypt cffi library) + warn("passlib.hash.bcrypt: Your installation of the %r backend is vulnerable to " + "the bsd wraparound bug, " + "and should be upgraded or replaced with another backend " + "(enabling workaround for now)." % backend, + uh.exc.PasslibSecurityWarning) + mixin_cls._has_2a_wraparound_bug = True + + #---------------------------------------------------------------- + # check for 2y support + #---------------------------------------------------------------- + test_hash_2y = TEST_HASH_2A.replace("2a", "2y") + result = safe_verify("test", test_hash_2y) + if result is NotImplemented: + mixin_cls._lacks_2y_support = True + log.debug("%r backend lacks $2y$ support, enabling workaround", backend) + elif not result: + raise RuntimeError("%s incorrectly rejected $2y$ hash" % backend) + else: + # NOTE: Not using this as fallback candidate, + # lacks wide enough support across implementations. + assert_lacks_8bit_bug(IDENT_2Y) + assert_lacks_wrap_bug(IDENT_2Y) + + #---------------------------------------------------------------- + # TODO: check for 2x support + #---------------------------------------------------------------- + + #---------------------------------------------------------------- + # check for 2b support + #---------------------------------------------------------------- + test_hash_2b = TEST_HASH_2A.replace("2a", "2b") + result = safe_verify("test", test_hash_2b) + if result is NotImplemented: + mixin_cls._lacks_2b_support = True + log.debug("%r backend lacks $2b$ support, enabling workaround", backend) + elif not result: + raise RuntimeError("%s incorrectly rejected $2b$ hash" % backend) + else: + mixin_cls._fallback_ident = IDENT_2B + assert_lacks_8bit_bug(IDENT_2B) + assert_lacks_wrap_bug(IDENT_2B) + + # set flag so we don't have to run this again + mixin_cls._workrounds_initialized = True + return True + + #=================================================================== + # digest calculation + #=================================================================== + + # _calc_checksum() defined by backends + + def _prepare_digest_args(self, secret): + """ + common helper for backends to implement _calc_checksum(). + takes in secret, returns (secret, ident) pair, + """ + return self._norm_digest_args(secret, self.ident, new=self.use_defaults) + + @classmethod + def _norm_digest_args(cls, secret, ident, new=False): + # make sure secret is unicode + require_valid_utf8_bytes = cls._require_valid_utf8_bytes + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + elif require_valid_utf8_bytes: + # if backend requires utf8 bytes (os_crypt); + # make sure input actually is utf8, or don't bother enabling utf-8 specific helpers. + try: + secret.decode("utf-8") + except UnicodeDecodeError: + # XXX: could just throw PasswordValueError here, backend will just do that + # when _calc_digest() is actually called. + require_valid_utf8_bytes = False + + # check max secret size + uh.validate_secret(secret) + + # check for truncation (during .hash() calls only) + if new: + cls._check_truncate_policy(secret) + + # NOTE: especially important to forbid NULLs for bcrypt, since many + # backends (bcryptor, bcrypt) happily accept them, and then + # silently truncate the password at first NULL they encounter! + if _BNULL in secret: + raise uh.exc.NullPasswordError(cls) + + # TODO: figure out way to skip these tests when not needed... + + # protect from wraparound bug by truncating secret before handing it to the backend. + # bcrypt only uses first 72 bytes anyways. + # NOTE: not needed for 2y/2b, but might use 2a as fallback for them. + if cls._has_2a_wraparound_bug and len(secret) >= 255: + if require_valid_utf8_bytes: + # backend requires valid utf8 bytes, so truncate secret to nearest valid segment. + # want to do this in constant time to not give away info about secret. + # NOTE: this only works because bcrypt will ignore everything past + # secret[71], so padding to include a full utf8 sequence + # won't break anything about the final output. + secret = utf8_truncate(secret, 72) + else: + secret = secret[:72] + + # special case handling for variants (ordered most common first) + if ident == IDENT_2A: + # nothing needs to be done. + pass + + elif ident == IDENT_2B: + if cls._lacks_2b_support: + # handle $2b$ hash format even if backend is too old. + # have it generate a 2A/2Y digest, then return it as a 2B hash. + # 2a-only backend could potentially exhibit wraparound bug -- + # but we work around that issue above. + ident = cls._fallback_ident + + elif ident == IDENT_2Y: + if cls._lacks_2y_support: + # handle $2y$ hash format (not supported by BSDs, being phased out on others) + # have it generate a 2A/2B digest, then return it as a 2Y hash. + ident = cls._fallback_ident + + elif ident == IDENT_2: + if cls._lacks_20_support: + # handle legacy $2$ format (not supported by most backends except BSD os_crypt) + # we can fake $2$ behavior using the 2A/2Y/2B algorithm + # by repeating the password until it's at least 72 chars in length. + if secret: + if require_valid_utf8_bytes: + # NOTE: this only works because bcrypt will ignore everything past + # secret[71], so padding to include a full utf8 sequence + # won't break anything about the final output. + secret = utf8_repeat_string(secret, 72) + else: + secret = repeat_string(secret, 72) + ident = cls._fallback_ident + + elif ident == IDENT_2X: + + # NOTE: shouldn't get here. + # XXX: could check if backend does actually offer 'support' + raise RuntimeError("$2x$ hashes not currently supported by passlib") + + else: + raise AssertionError("unexpected ident value: %r" % ident) + + return secret, ident + +#----------------------------------------------------------------------- +# stub backend +#----------------------------------------------------------------------- +class _NoBackend(_BcryptCommon): + """ + mixin used before any backend has been loaded. + contains stubs that force loading of one of the available backends. + """ + #=================================================================== + # digest calculation + #=================================================================== + def _calc_checksum(self, secret): + self._stub_requires_backend() + # NOTE: have to use super() here so that we don't recursively + # call subclass's wrapped _calc_checksum, e.g. bcrypt_sha256._calc_checksum + return super(bcrypt, self)._calc_checksum(secret) + + #=================================================================== + # eoc + #=================================================================== + +#----------------------------------------------------------------------- +# bcrypt backend +#----------------------------------------------------------------------- +class _BcryptBackend(_BcryptCommon): + """ + backend which uses 'bcrypt' package + """ + + @classmethod + def _load_backend_mixin(mixin_cls, name, dryrun): + # try to import bcrypt + global _bcrypt + if _detect_pybcrypt(): + # pybcrypt was installed instead + return False + try: + import bcrypt as _bcrypt + except ImportError: # pragma: no cover + return False + try: + version = _bcrypt.__about__.__version__ + except: + log.warning("(trapped) error reading bcrypt version", exc_info=True) + version = '' + + log.debug("detected 'bcrypt' backend, version %r", version) + return mixin_cls._finalize_backend_mixin(name, dryrun) + + # # TODO: would like to implementing verify() directly, + # # to skip need for parsing hash strings. + # # below method has a few edge cases where it chokes though. + # @classmethod + # def verify(cls, secret, hash): + # if isinstance(hash, unicode): + # hash = hash.encode("ascii") + # ident = hash[:hash.index(b"$", 1)+1].decode("ascii") + # if ident not in cls.ident_values: + # raise uh.exc.InvalidHashError(cls) + # secret, eff_ident = cls._norm_digest_args(secret, ident) + # if eff_ident != ident: + # # lacks support for original ident, replace w/ new one. + # hash = eff_ident.encode("ascii") + hash[len(ident):] + # result = _bcrypt.hashpw(secret, hash) + # assert result.startswith(eff_ident) + # return consteq(result, hash) + + def _calc_checksum(self, secret): + # bcrypt behavior: + # secret must be bytes + # config must be ascii bytes + # returns ascii bytes + secret, ident = self._prepare_digest_args(secret) + config = self._get_config(ident) + if isinstance(config, unicode): + config = config.encode("ascii") + hash = _bcrypt.hashpw(secret, config) + assert isinstance(hash, bytes) + if not hash.startswith(config) or len(hash) != len(config)+31: + raise uh.exc.CryptBackendError(self, config, hash, source="`bcrypt` package") + return hash[-31:].decode("ascii") + +#----------------------------------------------------------------------- +# bcryptor backend +#----------------------------------------------------------------------- +class _BcryptorBackend(_BcryptCommon): + """ + backend which uses 'bcryptor' package + """ + + @classmethod + def _load_backend_mixin(mixin_cls, name, dryrun): + # try to import bcryptor + global _bcryptor + try: + import bcryptor as _bcryptor + except ImportError: # pragma: no cover + return False + + # deprecated as of 1.7.2 + if not dryrun: + warn("Support for `bcryptor` is deprecated, and will be removed in Passlib 1.8; " + "Please use `pip install bcrypt` instead", DeprecationWarning) + + return mixin_cls._finalize_backend_mixin(name, dryrun) + + def _calc_checksum(self, secret): + # bcryptor behavior: + # py2: unicode secret/hash encoded as ascii bytes before use, + # bytes taken as-is; returns ascii bytes. + # py3: not supported + secret, ident = self._prepare_digest_args(secret) + config = self._get_config(ident) + hash = _bcryptor.engine.Engine(False).hash_key(secret, config) + if not hash.startswith(config) or len(hash) != len(config) + 31: + raise uh.exc.CryptBackendError(self, config, hash, source="bcryptor library") + return str_to_uascii(hash[-31:]) + +#----------------------------------------------------------------------- +# pybcrypt backend +#----------------------------------------------------------------------- +class _PyBcryptBackend(_BcryptCommon): + """ + backend which uses 'pybcrypt' package + """ + + #: classwide thread lock used for pybcrypt < 0.3 + _calc_lock = None + + @classmethod + def _load_backend_mixin(mixin_cls, name, dryrun): + # try to import pybcrypt + global _pybcrypt + if not _detect_pybcrypt(): + # not installed, or bcrypt installed instead + return False + try: + import bcrypt as _pybcrypt + except ImportError: # pragma: no cover + # XXX: should we raise AssertionError here? (if get here, _detect_pybcrypt() is broken) + return False + + # deprecated as of 1.7.2 + if not dryrun: + warn("Support for `py-bcrypt` is deprecated, and will be removed in Passlib 1.8; " + "Please use `pip install bcrypt` instead", DeprecationWarning) + + # determine pybcrypt version + try: + version = _pybcrypt._bcrypt.__version__ + except: + log.warning("(trapped) error reading pybcrypt version", exc_info=True) + version = "" + log.debug("detected 'pybcrypt' backend, version %r", version) + + # return calc function based on version + vinfo = parse_version(version) or (0, 0) + if vinfo < (0, 3): + warn("py-bcrypt %s has a major security vulnerability, " + "you should upgrade to py-bcrypt 0.3 immediately." + % version, uh.exc.PasslibSecurityWarning) + if mixin_cls._calc_lock is None: + import threading + mixin_cls._calc_lock = threading.Lock() + mixin_cls._calc_checksum = get_unbound_method_function(mixin_cls._calc_checksum_threadsafe) + + return mixin_cls._finalize_backend_mixin(name, dryrun) + + def _calc_checksum_threadsafe(self, secret): + # as workaround for pybcrypt < 0.3's concurrency issue, + # we wrap everything in a thread lock. as long as bcrypt is only + # used through passlib, this should be safe. + with self._calc_lock: + return self._calc_checksum_raw(secret) + + def _calc_checksum_raw(self, secret): + # py-bcrypt behavior: + # py2: unicode secret/hash encoded as ascii bytes before use, + # bytes taken as-is; returns ascii bytes. + # py3: unicode secret encoded as utf-8 bytes, + # hash encoded as ascii bytes, returns ascii unicode. + secret, ident = self._prepare_digest_args(secret) + config = self._get_config(ident) + hash = _pybcrypt.hashpw(secret, config) + if not hash.startswith(config) or len(hash) != len(config) + 31: + raise uh.exc.CryptBackendError(self, config, hash, source="pybcrypt library") + return str_to_uascii(hash[-31:]) + + _calc_checksum = _calc_checksum_raw + +#----------------------------------------------------------------------- +# os crypt backend +#----------------------------------------------------------------------- +class _OsCryptBackend(_BcryptCommon): + """ + backend which uses :func:`crypt.crypt` + """ + + #: set flag to ensure _prepare_digest_args() doesn't create invalid utf8 string + #: when truncating bytes. + _require_valid_utf8_bytes = not crypt_accepts_bytes + + @classmethod + def _load_backend_mixin(mixin_cls, name, dryrun): + if not test_crypt("test", TEST_HASH_2A): + return False + return mixin_cls._finalize_backend_mixin(name, dryrun) + + def _calc_checksum(self, secret): + # + # run secret through crypt.crypt(). + # if everything goes right, we'll get back a properly formed bcrypt hash. + # + secret, ident = self._prepare_digest_args(secret) + config = self._get_config(ident) + hash = safe_crypt(secret, config) + if hash is not None: + if not hash.startswith(config) or len(hash) != len(config) + 31: + raise uh.exc.CryptBackendError(self, config, hash) + return hash[-31:] + + # + # Check if this failed due to non-UTF8 bytes + # In detail: under py3, crypt.crypt() requires unicode inputs, which are then encoded to + # utf8 before passing them to os crypt() call. this is done according to the "s" format + # specifier for PyArg_ParseTuple (https://docs.python.org/3/c-api/arg.html). + # There appears no way to get around that to pass raw bytes; so we just throw error here + # to let user know they need to use another backend if they want raw bytes support. + # + # XXX: maybe just let safe_crypt() throw UnicodeDecodeError under passlib 2.0, + # and then catch it above? maybe have safe_crypt ALWAYS throw error + # instead of returning None? (would save re-detecting what went wrong) + # XXX: isn't secret ALWAYS bytes at this point? + # + if PY3 and isinstance(secret, bytes): + try: + secret.decode("utf-8") + except UnicodeDecodeError: + raise error_from(uh.exc.PasswordValueError( + "python3 crypt.crypt() ony supports bytes passwords using UTF8; " + "passlib recommends running `pip install bcrypt` for general bcrypt support.", + ), None) + + # + # else crypt() call failed for unknown reason. + # + # NOTE: getting here should be considered a bug in passlib -- + # if os_crypt backend detection said there's support, + # and we've already checked all known reasons above; + # want them to file bug so we can figure out what happened. + # in the meantime, users can avoid this by installing bcrypt-cffi backend; + # which won't have this (or utf8) edgecases. + # + # XXX: throw something more specific, like an "InternalBackendError"? + # NOTE: if do change this error, need to update test_81_crypt_fallback() expectations + # about what will be thrown; as well as safe_verify() above. + # + debug_only_repr = uh.exc.debug_only_repr + raise uh.exc.InternalBackendError( + "crypt.crypt() failed for unknown reason; " + "passlib recommends running `pip install bcrypt` for general bcrypt support." + # for debugging UTs -- + "(config=%s, secret=%s)" % (debug_only_repr(config), debug_only_repr(secret)), + ) + +#----------------------------------------------------------------------- +# builtin backend +#----------------------------------------------------------------------- +class _BuiltinBackend(_BcryptCommon): + """ + backend which uses passlib's pure-python implementation + """ + @classmethod + def _load_backend_mixin(mixin_cls, name, dryrun): + from passlib.utils import as_bool + if not as_bool(os.environ.get("PASSLIB_BUILTIN_BCRYPT")): + log.debug("bcrypt 'builtin' backend not enabled via $PASSLIB_BUILTIN_BCRYPT") + return False + global _builtin_bcrypt + from passlib.crypto._blowfish import raw_bcrypt as _builtin_bcrypt + return mixin_cls._finalize_backend_mixin(name, dryrun) + + def _calc_checksum(self, secret): + secret, ident = self._prepare_digest_args(secret) + chk = _builtin_bcrypt(secret, ident[1:-1], + self.salt.encode("ascii"), self.rounds) + return chk.decode("ascii") + +#============================================================================= +# handler +#============================================================================= +class bcrypt(_NoBackend, _BcryptCommon): + """This class implements the BCrypt password hash, and follows the :ref:`password-hash-api`. + + It supports a fixed-length salt, and a variable number of rounds. + + The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: + + :type salt: str + :param salt: + Optional salt string. + If not specified, one will be autogenerated (this is recommended). + If specified, it must be 22 characters, drawn from the regexp range ``[./0-9A-Za-z]``. + + :type rounds: int + :param rounds: + Optional number of rounds to use. + Defaults to 12, must be between 4 and 31, inclusive. + This value is logarithmic, the actual number of iterations used will be :samp:`2**{rounds}` + -- increasing the rounds by +1 will double the amount of time taken. + + :type ident: str + :param ident: + Specifies which version of the BCrypt algorithm will be used when creating a new hash. + Typically this option is not needed, as the default (``"2b"``) is usually the correct choice. + If specified, it must be one of the following: + + * ``"2"`` - the first revision of BCrypt, which suffers from a minor security flaw and is generally not used anymore. + * ``"2a"`` - some implementations suffered from rare security flaws, replaced by 2b. + * ``"2y"`` - format specific to the *crypt_blowfish* BCrypt implementation, + identical to ``"2b"`` in all but name. + * ``"2b"`` - latest revision of the official BCrypt algorithm, current default. + + :param bool truncate_error: + By default, BCrypt will silently truncate passwords larger than 72 bytes. + Setting ``truncate_error=True`` will cause :meth:`~passlib.ifc.PasswordHash.hash` + to raise a :exc:`~passlib.exc.PasswordTruncateError` instead. + + .. versionadded:: 1.7 + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include ``rounds`` + that are too small or too large, and ``salt`` strings that are too long. + + .. versionadded:: 1.6 + + .. versionchanged:: 1.6 + This class now supports ``"2y"`` hashes, and recognizes + (but does not support) the broken ``"2x"`` hashes. + (see the :ref:`crypt_blowfish bug ` + for details). + + .. versionchanged:: 1.6 + Added a pure-python backend. + + .. versionchanged:: 1.6.3 + + Added support for ``"2b"`` variant. + + .. versionchanged:: 1.7 + + Now defaults to ``"2b"`` variant. + """ + #============================================================================= + # backend + #============================================================================= + + # NOTE: the brunt of the bcrypt class is implemented in _BcryptCommon. + # there are then subclass for each backend (e.g. _PyBcryptBackend), + # these are dynamically prepended to this class's bases + # in order to load the appropriate backend. + + #: list of potential backends + backends = ("bcrypt", "pybcrypt", "bcryptor", "os_crypt", "builtin") + + #: flag that this class's bases should be modified by SubclassBackendMixin + _backend_mixin_target = True + + #: map of backend -> mixin class, used by _get_backend_loader() + _backend_mixin_map = { + None: _NoBackend, + "bcrypt": _BcryptBackend, + "pybcrypt": _PyBcryptBackend, + "bcryptor": _BcryptorBackend, + "os_crypt": _OsCryptBackend, + "builtin": _BuiltinBackend, + } + + #============================================================================= + # eoc + #============================================================================= + +#============================================================================= +# variants +#============================================================================= +_UDOLLAR = u("$") + +# XXX: it might be better to have all the bcrypt variants share a common base class, +# and have the (django_)bcrypt_sha256 wrappers just proxy bcrypt instead of subclassing it. +class _wrapped_bcrypt(bcrypt): + """ + abstracts out some bits bcrypt_sha256 & django_bcrypt_sha256 share. + - bypass backend-loading wrappers for hash() etc + - disable truncation support, sha256 wrappers don't need it. + """ + setting_kwds = tuple(elem for elem in bcrypt.setting_kwds if elem not in ["truncate_error"]) + truncate_size = None + + # XXX: these will be needed if any bcrypt backends directly implement this... + # @classmethod + # def hash(cls, secret, **kwds): + # # bypass bcrypt backend overriding this method + # # XXX: would wrapping bcrypt make this easier than subclassing it? + # return super(_BcryptCommon, cls).hash(secret, **kwds) + # + # @classmethod + # def verify(cls, secret, hash): + # # bypass bcrypt backend overriding this method + # return super(_BcryptCommon, cls).verify(secret, hash) + # + # @classmethod + # def genhash(cls, secret, hash): + # # bypass bcrypt backend overriding this method + # return super(_BcryptCommon, cls).genhash(secret, hash) + + @classmethod + def _check_truncate_policy(cls, secret): + # disable check performed by bcrypt(), since this doesn't truncate passwords. + pass + +#============================================================================= +# bcrypt sha256 wrapper +#============================================================================= + +class bcrypt_sha256(_wrapped_bcrypt): + """ + This class implements a composition of BCrypt + HMAC_SHA256, + and follows the :ref:`password-hash-api`. + + It supports a fixed-length salt, and a variable number of rounds. + + The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept + all the same optional keywords as the base :class:`bcrypt` hash. + + .. versionadded:: 1.6.2 + + .. versionchanged:: 1.7 + + Now defaults to ``"2b"`` bcrypt variant; though supports older hashes + generated using the ``"2a"`` bcrypt variant. + + .. versionchanged:: 1.7.3 + + For increased security, updated to use HMAC-SHA256 instead of plain SHA256. + Now only supports the ``"2b"`` bcrypt variant. Hash format updated to "v=2". + """ + #=================================================================== + # class attrs + #=================================================================== + + #-------------------- + # PasswordHash + #-------------------- + name = "bcrypt_sha256" + + #-------------------- + # GenericHandler + #-------------------- + # this is locked at 2b for now (with 2a allowed only for legacy v1 format) + ident_values = (IDENT_2A, IDENT_2B) + + # clone bcrypt's ident aliases so they can be used here as well... + ident_aliases = (lambda ident_values: dict(item for item in bcrypt.ident_aliases.items() + if item[1] in ident_values))(ident_values) + default_ident = IDENT_2B + + #-------------------- + # class specific + #-------------------- + + _supported_versions = set([1, 2]) + + #=================================================================== + # instance attrs + #=================================================================== + + #: wrapper version. + #: v1 -- used prior to passlib 1.7.3; performs ``bcrypt(sha256(secret), salt, cost)`` + #: v2 -- new in passlib 1.7.3; performs `bcrypt(sha256_hmac(salt, secret), salt, cost)`` + version = 2 + + #=================================================================== + # configuration + #=================================================================== + + @classmethod + def using(cls, version=None, **kwds): + subcls = super(bcrypt_sha256, cls).using(**kwds) + if version is not None: + subcls.version = subcls._norm_version(version) + ident = subcls.default_ident + if subcls.version > 1 and ident != IDENT_2B: + raise ValueError("bcrypt %r hashes not allowed for version %r" % + (ident, subcls.version)) + return subcls + + #=================================================================== + # formatting + #=================================================================== + + # sample hash: + # $bcrypt-sha256$2a,6$/3OeRpbOf8/l6nPPRdZPp.$nRiyYqPobEZGdNRBWihQhiFDh1ws1tu + # $bcrypt-sha256$ -- prefix/identifier + # 2a -- bcrypt variant + # , -- field separator + # 6 -- bcrypt work factor + # $ -- section separator + # /3OeRpbOf8/l6nPPRdZPp. -- salt + # $ -- section separator + # nRiyYqPobEZGdNRBWihQhiFDh1ws1tu -- digest + + # XXX: we can't use .ident attr due to bcrypt code using it. + # working around that via prefix. + prefix = u('$bcrypt-sha256$') + + #: current version 2 hash format + _v2_hash_re = re.compile(r"""(?x) + ^ + [$]bcrypt-sha256[$] + v=(?P\d+), + t=(?P2b), + r=(?P\d{1,2}) + [$](?P[^$]{22}) + (?:[$](?P[^$]{31}))? + $ + """) + + #: old version 1 hash format + _v1_hash_re = re.compile(r"""(?x) + ^ + [$]bcrypt-sha256[$] + (?P2[ab]), + (?P\d{1,2}) + [$](?P[^$]{22}) + (?:[$](?P[^$]{31}))? + $ + """) + + @classmethod + def identify(cls, hash): + hash = uh.to_unicode_for_identify(hash) + if not hash: + return False + return hash.startswith(cls.prefix) + + @classmethod + def from_string(cls, hash): + hash = to_unicode(hash, "ascii", "hash") + if not hash.startswith(cls.prefix): + raise uh.exc.InvalidHashError(cls) + m = cls._v2_hash_re.match(hash) + if m: + version = int(m.group("version")) + if version < 2: + raise uh.exc.MalformedHashError(cls) + else: + m = cls._v1_hash_re.match(hash) + if m: + version = 1 + else: + raise uh.exc.MalformedHashError(cls) + rounds = m.group("rounds") + if rounds.startswith(uh._UZERO) and rounds != uh._UZERO: + raise uh.exc.ZeroPaddedRoundsError(cls) + return cls( + version=version, + ident=m.group("type"), + rounds=int(rounds), + salt=m.group("salt"), + checksum=m.group("digest"), + ) + + _v2_template = u("$bcrypt-sha256$v=2,t=%s,r=%d$%s$%s") + _v1_template = u("$bcrypt-sha256$%s,%d$%s$%s") + + def to_string(self): + if self.version == 1: + template = self._v1_template + else: + template = self._v2_template + hash = template % (self.ident.strip(_UDOLLAR), self.rounds, self.salt, self.checksum) + return uascii_to_str(hash) + + #=================================================================== + # init + #=================================================================== + + def __init__(self, version=None, **kwds): + if version is not None: + self.version = self._norm_version(version) + super(bcrypt_sha256, self).__init__(**kwds) + + #=================================================================== + # version + #=================================================================== + + @classmethod + def _norm_version(cls, version): + if version not in cls._supported_versions: + raise ValueError("%s: unknown or unsupported version: %r" % (cls.name, version)) + return version + + #=================================================================== + # checksum + #=================================================================== + + def _calc_checksum(self, secret): + # NOTE: can't use digest directly, since bcrypt stops at first NULL. + # NOTE: bcrypt doesn't fully mix entropy for bytes 55-72 of password + # (XXX: citation needed), so we don't want key to be > 55 bytes. + # thus, have to use base64 (44 bytes) rather than hex (64 bytes). + # XXX: it's later come out that 55-72 may be ok, so later revision of bcrypt_sha256 + # may switch to hex encoding, since it's simpler to implement elsewhere. + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + + if self.version == 1: + # version 1 -- old version just ran secret through sha256(), + # though this could be vulnerable to a breach attach + # (c.f. issue 114); which is why v2 switched to hmac wrapper. + digest = sha256(secret).digest() + else: + # version 2 -- running secret through HMAC keyed off salt. + # this prevents known secret -> sha256 password tables from being + # used to test against a bcrypt_sha256 hash. + # keying off salt (instead of constant string) should minimize chances of this + # colliding with existing table of hmac digest lookups as well. + # NOTE: salt in this case is the "bcrypt64"-encoded value, not the raw salt bytes, + # to make things easier for parallel implementations of this hash -- + # saving them the trouble of implementing a "bcrypt64" decoder. + salt = self.salt + if salt[-1] not in self.final_salt_chars: + # forbidding salts with padding bits set, because bcrypt implementations + # won't consistently hash them the same. since we control this format, + # just prevent these from even getting used. + raise ValueError("invalid salt string") + digest = compile_hmac("sha256", salt.encode("ascii"))(secret) + + # NOTE: output of b64encode() uses "+/" altchars, "=" padding chars, + # and no leading/trailing whitespace. + key = b64encode(digest) + + # hand result off to normal bcrypt algorithm + return super(bcrypt_sha256, self)._calc_checksum(key) + + #=================================================================== + # other + #=================================================================== + + def _calc_needs_update(self, **kwds): + if self.version < type(self).version: + return True + return super(bcrypt_sha256, self)._calc_needs_update(**kwds) + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/handlers/cisco.py b/ansible/lib/python3.11/site-packages/passlib/handlers/cisco.py new file mode 100644 index 000000000..e715e1ab5 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/handlers/cisco.py @@ -0,0 +1,440 @@ +""" +passlib.handlers.cisco -- Cisco password hashes +""" +#============================================================================= +# imports +#============================================================================= +# core +from binascii import hexlify, unhexlify +from hashlib import md5 +import logging; log = logging.getLogger(__name__) +from warnings import warn +# site +# pkg +from passlib.utils import right_pad_string, to_unicode, repeat_string, to_bytes +from passlib.utils.binary import h64 +from passlib.utils.compat import unicode, u, join_byte_values, \ + join_byte_elems, iter_byte_values, uascii_to_str +import passlib.utils.handlers as uh +# local +__all__ = [ + "cisco_pix", + "cisco_asa", + "cisco_type7", +] + +#============================================================================= +# utils +#============================================================================= + +#: dummy bytes used by spoil_digest var in cisco_pix._calc_checksum() +_DUMMY_BYTES = b'\xFF' * 32 + +#============================================================================= +# cisco pix firewall hash +#============================================================================= +class cisco_pix(uh.HasUserContext, uh.StaticHandler): + """ + This class implements the password hash used by older Cisco PIX firewalls, + and follows the :ref:`password-hash-api`. + It does a single round of hashing, and relies on the username + as the salt. + + This class only allows passwords <= 16 bytes, anything larger + will result in a :exc:`~passlib.exc.PasswordSizeError` if passed to :meth:`~cisco_pix.hash`, + and be silently rejected if passed to :meth:`~cisco_pix.verify`. + + The :meth:`~passlib.ifc.PasswordHash.hash`, + :meth:`~passlib.ifc.PasswordHash.genhash`, and + :meth:`~passlib.ifc.PasswordHash.verify` methods + all support the following extra keyword: + + :param str user: + String containing name of user account this password is associated with. + + This is *required* in order to correctly hash passwords associated + with a user account on the Cisco device, as it is used to salt + the hash. + + Conversely, this *must* be omitted or set to ``""`` in order to correctly + hash passwords which don't have an associated user account + (such as the "enable" password). + + .. versionadded:: 1.6 + + .. versionchanged:: 1.7.1 + + Passwords > 16 bytes are now rejected / throw error instead of being silently truncated, + to match Cisco behavior. A number of :ref:`bugs ` were fixed + which caused prior releases to generate unverifiable hashes in certain cases. + """ + #=================================================================== + # class attrs + #=================================================================== + + #-------------------- + # PasswordHash + #-------------------- + name = "cisco_pix" + + truncate_size = 16 + + # NOTE: these are the default policy for PasswordHash, + # but want to set them explicitly for now. + truncate_error = True + truncate_verify_reject = True + + #-------------------- + # GenericHandler + #-------------------- + checksum_size = 16 + checksum_chars = uh.HASH64_CHARS + + #-------------------- + # custom + #-------------------- + + #: control flag signalling "cisco_asa" mode, set by cisco_asa class + _is_asa = False + + #=================================================================== + # methods + #=================================================================== + def _calc_checksum(self, secret): + """ + This function implements the "encrypted" hash format used by Cisco + PIX & ASA. It's behavior has been confirmed for ASA 9.6, + but is presumed correct for PIX & other ASA releases, + as it fits with known test vectors, and existing literature. + + While nearly the same, the PIX & ASA hashes have slight differences, + so this function performs differently based on the _is_asa class flag. + Noteable changes from PIX to ASA include password size limit + increased from 16 -> 32, and other internal changes. + """ + # select PIX vs or ASA mode + asa = self._is_asa + + # + # encode secret + # + # per ASA 8.4 documentation, + # http://www.cisco.com/c/en/us/td/docs/security/asa/asa84/configuration/guide/asa_84_cli_config/ref_cli.html#Supported_Character_Sets, + # it supposedly uses UTF-8 -- though some double-encoding issues have + # been observed when trying to actually *set* a non-ascii password + # via ASDM, and access via SSH seems to strip 8-bit chars. + # + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + + # + # check if password too large + # + # Per ASA 9.6 changes listed in + # http://www.cisco.com/c/en/us/td/docs/security/asa/roadmap/asa_new_features.html, + # prior releases had a maximum limit of 32 characters. + # Testing with an ASA 9.6 system bears this out -- + # setting 32-char password for a user account, + # and logins will fail if any chars are appended. + # (ASA 9.6 added new PBKDF2-based hash algorithm, + # which supports larger passwords). + # + # Per PIX documentation + # http://www.cisco.com/en/US/docs/security/pix/pix50/configuration/guide/commands.html, + # it would not allow passwords > 16 chars. + # + # Thus, we unconditionally throw a password size error here, + # as nothing valid can come from a larger password. + # NOTE: assuming PIX has same behavior, but at 16 char limit. + # + spoil_digest = None + if len(secret) > self.truncate_size: + if self.use_defaults: + # called from hash() + msg = "Password too long (%s allows at most %d bytes)" % \ + (self.name, self.truncate_size) + raise uh.exc.PasswordSizeError(self.truncate_size, msg=msg) + else: + # called from verify() -- + # We don't want to throw error, or return early, + # as that would let attacker know too much. Instead, we set a + # flag to add some dummy data into the md5 digest, so that + # output won't match truncated version of secret, or anything + # else that's fixed and predictable. + spoil_digest = secret + _DUMMY_BYTES + + # + # append user to secret + # + # Policy appears to be: + # + # * Nothing appended for enable password (user = "") + # + # * ASA: If user present, but secret is >= 28 chars, nothing appended. + # + # * 1-2 byte users not allowed. + # DEVIATION: we're letting them through, and repeating their + # chars ala 3-char user, to simplify testing. + # Could issue warning in the future though. + # + # * 3 byte user has first char repeated, to pad to 4. + # (observed under ASA 9.6, assuming true elsewhere) + # + # * 4 byte users are used directly. + # + # * 5+ byte users are truncated to 4 bytes. + # + user = self.user + if user: + if isinstance(user, unicode): + user = user.encode("utf-8") + if not asa or len(secret) < 28: + secret += repeat_string(user, 4) + + # + # pad / truncate result to limit + # + # While PIX always pads to 16 bytes, ASA increases to 32 bytes IFF + # secret+user > 16 bytes. This makes PIX & ASA have different results + # where secret size in range(13,16), and user is present -- + # PIX will truncate to 16, ASA will truncate to 32. + # + if asa and len(secret) > 16: + pad_size = 32 + else: + pad_size = 16 + secret = right_pad_string(secret, pad_size) + + # + # md5 digest + # + if spoil_digest: + # make sure digest won't match truncated version of secret + secret += spoil_digest + digest = md5(secret).digest() + + # + # drop every 4th byte + # NOTE: guessing this was done because it makes output exactly + # 16 bytes, which may have been a general 'char password[]' + # size limit under PIX + # + digest = join_byte_elems(c for i, c in enumerate(digest) if (i + 1) & 3) + + # + # encode using Hash64 + # + return h64.encode_bytes(digest).decode("ascii") + + # NOTE: works, but needs UTs. + # @classmethod + # def same_as_pix(cls, secret, user=""): + # """ + # test whether (secret + user) combination should + # have the same hash under PIX and ASA. + # + # mainly present to help unittests. + # """ + # # see _calc_checksum() above for details of this logic. + # size = len(to_bytes(secret, "utf-8")) + # if user and size < 28: + # size += 4 + # return size < 17 + + #=================================================================== + # eoc + #=================================================================== + + +class cisco_asa(cisco_pix): + """ + This class implements the password hash used by Cisco ASA/PIX 7.0 and newer (2005). + Aside from a different internal algorithm, it's use and format is identical + to the older :class:`cisco_pix` class. + + For passwords less than 13 characters, this should be identical to :class:`!cisco_pix`, + but will generate a different hash for most larger inputs + (See the `Format & Algorithm`_ section for the details). + + This class only allows passwords <= 32 bytes, anything larger + will result in a :exc:`~passlib.exc.PasswordSizeError` if passed to :meth:`~cisco_asa.hash`, + and be silently rejected if passed to :meth:`~cisco_asa.verify`. + + .. versionadded:: 1.7 + + .. versionchanged:: 1.7.1 + + Passwords > 32 bytes are now rejected / throw error instead of being silently truncated, + to match Cisco behavior. A number of :ref:`bugs ` were fixed + which caused prior releases to generate unverifiable hashes in certain cases. + """ + #=================================================================== + # class attrs + #=================================================================== + + #-------------------- + # PasswordHash + #-------------------- + name = "cisco_asa" + + #-------------------- + # TruncateMixin + #-------------------- + truncate_size = 32 + + #-------------------- + # cisco_pix + #-------------------- + _is_asa = True + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# type 7 +#============================================================================= +class cisco_type7(uh.GenericHandler): + """ + This class implements the "Type 7" password encoding used by Cisco IOS, + and follows the :ref:`password-hash-api`. + It has a simple 4-5 bit salt, but is nonetheless a reversible encoding + instead of a real hash. + + The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: + + :type salt: int + :param salt: + This may be an optional salt integer drawn from ``range(0,16)``. + If omitted, one will be chosen at random. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include + ``salt`` values that are out of range. + + Note that while this class outputs digests in upper-case hexadecimal, + it will accept lower-case as well. + + This class also provides the following additional method: + + .. automethod:: decode + """ + #=================================================================== + # class attrs + #=================================================================== + + #-------------------- + # PasswordHash + #-------------------- + name = "cisco_type7" + setting_kwds = ("salt",) + + #-------------------- + # GenericHandler + #-------------------- + checksum_chars = uh.UPPER_HEX_CHARS + + #-------------------- + # HasSalt + #-------------------- + + # NOTE: encoding could handle max_salt_value=99, but since key is only 52 + # chars in size, not sure what appropriate behavior is for that edge case. + min_salt_value = 0 + max_salt_value = 52 + + #=================================================================== + # methods + #=================================================================== + @classmethod + def using(cls, salt=None, **kwds): + subcls = super(cisco_type7, cls).using(**kwds) + if salt is not None: + salt = subcls._norm_salt(salt, relaxed=kwds.get("relaxed")) + subcls._generate_salt = staticmethod(lambda: salt) + return subcls + + @classmethod + def from_string(cls, hash): + hash = to_unicode(hash, "ascii", "hash") + if len(hash) < 2: + raise uh.exc.InvalidHashError(cls) + salt = int(hash[:2]) # may throw ValueError + return cls(salt=salt, checksum=hash[2:].upper()) + + def __init__(self, salt=None, **kwds): + super(cisco_type7, self).__init__(**kwds) + if salt is not None: + salt = self._norm_salt(salt) + elif self.use_defaults: + salt = self._generate_salt() + assert self._norm_salt(salt) == salt, "generated invalid salt: %r" % (salt,) + else: + raise TypeError("no salt specified") + self.salt = salt + + @classmethod + def _norm_salt(cls, salt, relaxed=False): + """ + validate & normalize salt value. + .. note:: + the salt for this algorithm is an integer 0-52, not a string + """ + if not isinstance(salt, int): + raise uh.exc.ExpectedTypeError(salt, "integer", "salt") + if 0 <= salt <= cls.max_salt_value: + return salt + msg = "salt/offset must be in 0..52 range" + if relaxed: + warn(msg, uh.PasslibHashWarning) + return 0 if salt < 0 else cls.max_salt_value + else: + raise ValueError(msg) + + @staticmethod + def _generate_salt(): + return uh.rng.randint(0, 15) + + def to_string(self): + return "%02d%s" % (self.salt, uascii_to_str(self.checksum)) + + def _calc_checksum(self, secret): + # XXX: no idea what unicode policy is, but all examples are + # 7-bit ascii compatible, so using UTF-8 + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + return hexlify(self._cipher(secret, self.salt)).decode("ascii").upper() + + @classmethod + def decode(cls, hash, encoding="utf-8"): + """decode hash, returning original password. + + :arg hash: encoded password + :param encoding: optional encoding to use (defaults to ``UTF-8``). + :returns: password as unicode + """ + self = cls.from_string(hash) + tmp = unhexlify(self.checksum.encode("ascii")) + raw = self._cipher(tmp, self.salt) + return raw.decode(encoding) if encoding else raw + + # type7 uses a xor-based vingere variant, using the following secret key: + _key = u("dsfd;kfoA,.iyewrkldJKDHSUBsgvca69834ncxv9873254k;fg87") + + @classmethod + def _cipher(cls, data, salt): + """xor static key against data - encrypts & decrypts""" + key = cls._key + key_size = len(key) + return join_byte_values( + value ^ ord(key[(salt + idx) % key_size]) + for idx, value in enumerate(iter_byte_values(data)) + ) + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/handlers/des_crypt.py b/ansible/lib/python3.11/site-packages/passlib/handlers/des_crypt.py new file mode 100644 index 000000000..68a4ca7ee --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/handlers/des_crypt.py @@ -0,0 +1,607 @@ +"""passlib.handlers.des_crypt - traditional unix (DES) crypt and variants""" +#============================================================================= +# imports +#============================================================================= +# core +import re +import logging; log = logging.getLogger(__name__) +from warnings import warn +# site +# pkg +from passlib.utils import safe_crypt, test_crypt, to_unicode +from passlib.utils.binary import h64, h64big +from passlib.utils.compat import byte_elem_value, u, uascii_to_str, unicode, suppress_cause +from passlib.crypto.des import des_encrypt_int_block +import passlib.utils.handlers as uh +# local +__all__ = [ + "des_crypt", + "bsdi_crypt", + "bigcrypt", + "crypt16", +] + +#============================================================================= +# pure-python backend for des_crypt family +#============================================================================= +_BNULL = b'\x00' + +def _crypt_secret_to_key(secret): + """convert secret to 64-bit DES key. + + this only uses the first 8 bytes of the secret, + and discards the high 8th bit of each byte at that. + a null parity bit is inserted after every 7th bit of the output. + """ + # NOTE: this would set the parity bits correctly, + # but des_encrypt_int_block() would just ignore them... + ##return sum(expand_7bit(byte_elem_value(c) & 0x7f) << (56-i*8) + ## for i, c in enumerate(secret[:8])) + return sum((byte_elem_value(c) & 0x7f) << (57-i*8) + for i, c in enumerate(secret[:8])) + +def _raw_des_crypt(secret, salt): + """pure-python backed for des_crypt""" + assert len(salt) == 2 + + # NOTE: some OSes will accept non-HASH64 characters in the salt, + # but what value they assign these characters varies wildy, + # so just rejecting them outright. + # the same goes for single-character salts... + # some OSes duplicate the char, some insert a '.' char, + # and openbsd does (something) which creates an invalid hash. + salt_value = h64.decode_int12(salt) + + # gotta do something - no official policy since this predates unicode + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + assert isinstance(secret, bytes) + + # forbidding NULL char because underlying crypt() rejects them too. + if _BNULL in secret: + raise uh.exc.NullPasswordError(des_crypt) + + # convert first 8 bytes of secret string into an integer + key_value = _crypt_secret_to_key(secret) + + # run data through des using input of 0 + result = des_encrypt_int_block(key_value, 0, salt_value, 25) + + # run h64 encode on result + return h64big.encode_int64(result) + +def _bsdi_secret_to_key(secret): + """convert secret to DES key used by bsdi_crypt""" + key_value = _crypt_secret_to_key(secret) + idx = 8 + end = len(secret) + while idx < end: + next = idx + 8 + tmp_value = _crypt_secret_to_key(secret[idx:next]) + key_value = des_encrypt_int_block(key_value, key_value) ^ tmp_value + idx = next + return key_value + +def _raw_bsdi_crypt(secret, rounds, salt): + """pure-python backend for bsdi_crypt""" + + # decode salt + salt_value = h64.decode_int24(salt) + + # gotta do something - no official policy since this predates unicode + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + assert isinstance(secret, bytes) + + # forbidding NULL char because underlying crypt() rejects them too. + if _BNULL in secret: + raise uh.exc.NullPasswordError(bsdi_crypt) + + # convert secret string into an integer + key_value = _bsdi_secret_to_key(secret) + + # run data through des using input of 0 + result = des_encrypt_int_block(key_value, 0, salt_value, rounds) + + # run h64 encode on result + return h64big.encode_int64(result) + +#============================================================================= +# handlers +#============================================================================= +class des_crypt(uh.TruncateMixin, uh.HasManyBackends, uh.HasSalt, uh.GenericHandler): + """This class implements the des-crypt password hash, and follows the :ref:`password-hash-api`. + + It supports a fixed-length salt. + + The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: + + :type salt: str + :param salt: + Optional salt string. + If not specified, one will be autogenerated (this is recommended). + If specified, it must be 2 characters, drawn from the regexp range ``[./0-9A-Za-z]``. + + :param bool truncate_error: + By default, des_crypt will silently truncate passwords larger than 8 bytes. + Setting ``truncate_error=True`` will cause :meth:`~passlib.ifc.PasswordHash.hash` + to raise a :exc:`~passlib.exc.PasswordTruncateError` instead. + + .. versionadded:: 1.7 + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include + ``salt`` strings that are too long. + + .. versionadded:: 1.6 + """ + #=================================================================== + # class attrs + #=================================================================== + + #-------------------- + # PasswordHash + #-------------------- + name = "des_crypt" + setting_kwds = ("salt", "truncate_error") + + #-------------------- + # GenericHandler + #-------------------- + checksum_chars = uh.HASH64_CHARS + checksum_size = 11 + + #-------------------- + # HasSalt + #-------------------- + min_salt_size = max_salt_size = 2 + salt_chars = uh.HASH64_CHARS + + #-------------------- + # TruncateMixin + #-------------------- + truncate_size = 8 + + #=================================================================== + # formatting + #=================================================================== + # FORMAT: 2 chars of H64-encoded salt + 11 chars of H64-encoded checksum + + _hash_regex = re.compile(u(r""" + ^ + (?P[./a-z0-9]{2}) + (?P[./a-z0-9]{11})? + $"""), re.X|re.I) + + @classmethod + def from_string(cls, hash): + hash = to_unicode(hash, "ascii", "hash") + salt, chk = hash[:2], hash[2:] + return cls(salt=salt, checksum=chk or None) + + def to_string(self): + hash = u("%s%s") % (self.salt, self.checksum) + return uascii_to_str(hash) + + #=================================================================== + # digest calculation + #=================================================================== + def _calc_checksum(self, secret): + # check for truncation (during .hash() calls only) + if self.use_defaults: + self._check_truncate_policy(secret) + + return self._calc_checksum_backend(secret) + + #=================================================================== + # backend + #=================================================================== + backends = ("os_crypt", "builtin") + + #--------------------------------------------------------------- + # os_crypt backend + #--------------------------------------------------------------- + @classmethod + def _load_backend_os_crypt(cls): + if test_crypt("test", 'abgOeLfPimXQo'): + cls._set_calc_checksum_backend(cls._calc_checksum_os_crypt) + return True + else: + return False + + def _calc_checksum_os_crypt(self, secret): + # NOTE: we let safe_crypt() encode unicode secret -> utf8; + # no official policy since des-crypt predates unicode + hash = safe_crypt(secret, self.salt) + if hash is None: + # py3's crypt.crypt() can't handle non-utf8 bytes. + # fallback to builtin alg, which is always available. + return self._calc_checksum_builtin(secret) + if not hash.startswith(self.salt) or len(hash) != 13: + raise uh.exc.CryptBackendError(self, self.salt, hash) + return hash[2:] + + #--------------------------------------------------------------- + # builtin backend + #--------------------------------------------------------------- + @classmethod + def _load_backend_builtin(cls): + cls._set_calc_checksum_backend(cls._calc_checksum_builtin) + return True + + def _calc_checksum_builtin(self, secret): + return _raw_des_crypt(secret, self.salt.encode("ascii")).decode("ascii") + + #=================================================================== + # eoc + #=================================================================== + +class bsdi_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler): + """This class implements the BSDi-Crypt password hash, and follows the :ref:`password-hash-api`. + + It supports a fixed-length salt, and a variable number of rounds. + + The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: + + :type salt: str + :param salt: + Optional salt string. + If not specified, one will be autogenerated (this is recommended). + If specified, it must be 4 characters, drawn from the regexp range ``[./0-9A-Za-z]``. + + :type rounds: int + :param rounds: + Optional number of rounds to use. + Defaults to 5001, must be between 1 and 16777215, inclusive. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include ``rounds`` + that are too small or too large, and ``salt`` strings that are too long. + + .. versionadded:: 1.6 + + .. versionchanged:: 1.6 + :meth:`hash` will now issue a warning if an even number of rounds is used + (see :ref:`bsdi-crypt-security-issues` regarding weak DES keys). + """ + #=================================================================== + # class attrs + #=================================================================== + #--GenericHandler-- + name = "bsdi_crypt" + setting_kwds = ("salt", "rounds") + checksum_size = 11 + checksum_chars = uh.HASH64_CHARS + + #--HasSalt-- + min_salt_size = max_salt_size = 4 + salt_chars = uh.HASH64_CHARS + + #--HasRounds-- + default_rounds = 5001 + min_rounds = 1 + max_rounds = 16777215 # (1<<24)-1 + rounds_cost = "linear" + + # NOTE: OpenBSD login.conf reports 7250 as minimum allowed rounds, + # but that seems to be an OS policy, not a algorithm limitation. + + #=================================================================== + # parsing + #=================================================================== + _hash_regex = re.compile(u(r""" + ^ + _ + (?P[./a-z0-9]{4}) + (?P[./a-z0-9]{4}) + (?P[./a-z0-9]{11})? + $"""), re.X|re.I) + + @classmethod + def from_string(cls, hash): + hash = to_unicode(hash, "ascii", "hash") + m = cls._hash_regex.match(hash) + if not m: + raise uh.exc.InvalidHashError(cls) + rounds, salt, chk = m.group("rounds", "salt", "chk") + return cls( + rounds=h64.decode_int24(rounds.encode("ascii")), + salt=salt, + checksum=chk, + ) + + def to_string(self): + hash = u("_%s%s%s") % (h64.encode_int24(self.rounds).decode("ascii"), + self.salt, self.checksum) + return uascii_to_str(hash) + + #=================================================================== + # validation + #=================================================================== + + # NOTE: keeping this flag for admin/choose_rounds.py script. + # want to eventually expose rounds logic to that script in better way. + _avoid_even_rounds = True + + @classmethod + def using(cls, **kwds): + subcls = super(bsdi_crypt, cls).using(**kwds) + if not subcls.default_rounds & 1: + # issue warning if caller set an even 'rounds' value. + warn("bsdi_crypt rounds should be odd, as even rounds may reveal weak DES keys", + uh.exc.PasslibSecurityWarning) + return subcls + + @classmethod + def _generate_rounds(cls): + rounds = super(bsdi_crypt, cls)._generate_rounds() + # ensure autogenerated rounds are always odd + # NOTE: doing this even for default_rounds so needs_update() doesn't get + # caught in a loop. + # FIXME: this technically might generate a rounds value 1 larger + # than the requested upper bound - but better to err on side of safety. + return rounds|1 + + #=================================================================== + # migration + #=================================================================== + + def _calc_needs_update(self, **kwds): + # mark bsdi_crypt hashes as deprecated if they have even rounds. + if not self.rounds & 1: + return True + # hand off to base implementation + return super(bsdi_crypt, self)._calc_needs_update(**kwds) + + #=================================================================== + # backends + #=================================================================== + backends = ("os_crypt", "builtin") + + #--------------------------------------------------------------- + # os_crypt backend + #--------------------------------------------------------------- + @classmethod + def _load_backend_os_crypt(cls): + if test_crypt("test", '_/...lLDAxARksGCHin.'): + cls._set_calc_checksum_backend(cls._calc_checksum_os_crypt) + return True + else: + return False + + def _calc_checksum_os_crypt(self, secret): + config = self.to_string() + hash = safe_crypt(secret, config) + if hash is None: + # py3's crypt.crypt() can't handle non-utf8 bytes. + # fallback to builtin alg, which is always available. + return self._calc_checksum_builtin(secret) + if not hash.startswith(config[:9]) or len(hash) != 20: + raise uh.exc.CryptBackendError(self, config, hash) + return hash[-11:] + + #--------------------------------------------------------------- + # builtin backend + #--------------------------------------------------------------- + @classmethod + def _load_backend_builtin(cls): + cls._set_calc_checksum_backend(cls._calc_checksum_builtin) + return True + + def _calc_checksum_builtin(self, secret): + return _raw_bsdi_crypt(secret, self.rounds, self.salt.encode("ascii")).decode("ascii") + + #=================================================================== + # eoc + #=================================================================== + +class bigcrypt(uh.HasSalt, uh.GenericHandler): + """This class implements the BigCrypt password hash, and follows the :ref:`password-hash-api`. + + It supports a fixed-length salt. + + The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: + + :type salt: str + :param salt: + Optional salt string. + If not specified, one will be autogenerated (this is recommended). + If specified, it must be 22 characters, drawn from the regexp range ``[./0-9A-Za-z]``. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include + ``salt`` strings that are too long. + + .. versionadded:: 1.6 + """ + #=================================================================== + # class attrs + #=================================================================== + #--GenericHandler-- + name = "bigcrypt" + setting_kwds = ("salt",) + checksum_chars = uh.HASH64_CHARS + # NOTE: checksum chars must be multiple of 11 + + #--HasSalt-- + min_salt_size = max_salt_size = 2 + salt_chars = uh.HASH64_CHARS + + #=================================================================== + # internal helpers + #=================================================================== + _hash_regex = re.compile(u(r""" + ^ + (?P[./a-z0-9]{2}) + (?P([./a-z0-9]{11})+)? + $"""), re.X|re.I) + + @classmethod + def from_string(cls, hash): + hash = to_unicode(hash, "ascii", "hash") + m = cls._hash_regex.match(hash) + if not m: + raise uh.exc.InvalidHashError(cls) + salt, chk = m.group("salt", "chk") + return cls(salt=salt, checksum=chk) + + def to_string(self): + hash = u("%s%s") % (self.salt, self.checksum) + return uascii_to_str(hash) + + def _norm_checksum(self, checksum, relaxed=False): + checksum = super(bigcrypt, self)._norm_checksum(checksum, relaxed=relaxed) + if len(checksum) % 11: + raise uh.exc.InvalidHashError(self) + return checksum + + #=================================================================== + # backend + #=================================================================== + def _calc_checksum(self, secret): + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + chk = _raw_des_crypt(secret, self.salt.encode("ascii")) + idx = 8 + end = len(secret) + while idx < end: + next = idx + 8 + chk += _raw_des_crypt(secret[idx:next], chk[-11:-9]) + idx = next + return chk.decode("ascii") + + #=================================================================== + # eoc + #=================================================================== + +class crypt16(uh.TruncateMixin, uh.HasSalt, uh.GenericHandler): + """This class implements the crypt16 password hash, and follows the :ref:`password-hash-api`. + + It supports a fixed-length salt. + + The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: + + :type salt: str + :param salt: + Optional salt string. + If not specified, one will be autogenerated (this is recommended). + If specified, it must be 2 characters, drawn from the regexp range ``[./0-9A-Za-z]``. + + :param bool truncate_error: + By default, crypt16 will silently truncate passwords larger than 16 bytes. + Setting ``truncate_error=True`` will cause :meth:`~passlib.ifc.PasswordHash.hash` + to raise a :exc:`~passlib.exc.PasswordTruncateError` instead. + + .. versionadded:: 1.7 + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include + ``salt`` strings that are too long. + + .. versionadded:: 1.6 + """ + #=================================================================== + # class attrs + #=================================================================== + + #-------------------- + # PasswordHash + #-------------------- + name = "crypt16" + setting_kwds = ("salt", "truncate_error") + + #-------------------- + # GenericHandler + #-------------------- + checksum_size = 22 + checksum_chars = uh.HASH64_CHARS + + #-------------------- + # HasSalt + #-------------------- + min_salt_size = max_salt_size = 2 + salt_chars = uh.HASH64_CHARS + + #-------------------- + # TruncateMixin + #-------------------- + truncate_size = 16 + + #=================================================================== + # internal helpers + #=================================================================== + _hash_regex = re.compile(u(r""" + ^ + (?P[./a-z0-9]{2}) + (?P[./a-z0-9]{22})? + $"""), re.X|re.I) + + @classmethod + def from_string(cls, hash): + hash = to_unicode(hash, "ascii", "hash") + m = cls._hash_regex.match(hash) + if not m: + raise uh.exc.InvalidHashError(cls) + salt, chk = m.group("salt", "chk") + return cls(salt=salt, checksum=chk) + + def to_string(self): + hash = u("%s%s") % (self.salt, self.checksum) + return uascii_to_str(hash) + + #=================================================================== + # backend + #=================================================================== + def _calc_checksum(self, secret): + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + + # check for truncation (during .hash() calls only) + if self.use_defaults: + self._check_truncate_policy(secret) + + # parse salt value + try: + salt_value = h64.decode_int12(self.salt.encode("ascii")) + except ValueError: # pragma: no cover - caught by class + raise suppress_cause(ValueError("invalid chars in salt")) + + # convert first 8 byts of secret string into an integer, + key1 = _crypt_secret_to_key(secret) + + # run data through des using input of 0 + result1 = des_encrypt_int_block(key1, 0, salt_value, 20) + + # convert next 8 bytes of secret string into integer (key=0 if secret < 8 chars) + key2 = _crypt_secret_to_key(secret[8:16]) + + # run data through des using input of 0 + result2 = des_encrypt_int_block(key2, 0, salt_value, 5) + + # done + chk = h64big.encode_int64(result1) + h64big.encode_int64(result2) + return chk.decode("ascii") + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/handlers/digests.py b/ansible/lib/python3.11/site-packages/passlib/handlers/digests.py new file mode 100644 index 000000000..982155c91 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/handlers/digests.py @@ -0,0 +1,168 @@ +"""passlib.handlers.digests - plain hash digests +""" +#============================================================================= +# imports +#============================================================================= +# core +import hashlib +import logging; log = logging.getLogger(__name__) +# site +# pkg +from passlib.utils import to_native_str, to_bytes, render_bytes, consteq +from passlib.utils.compat import unicode, str_to_uascii +import passlib.utils.handlers as uh +from passlib.crypto.digest import lookup_hash +# local +__all__ = [ + "create_hex_hash", + "hex_md4", + "hex_md5", + "hex_sha1", + "hex_sha256", + "hex_sha512", +] + +#============================================================================= +# helpers for hexadecimal hashes +#============================================================================= +class HexDigestHash(uh.StaticHandler): + """this provides a template for supporting passwords stored as plain hexadecimal hashes""" + #=================================================================== + # class attrs + #=================================================================== + _hash_func = None # hash function to use - filled in by create_hex_hash() + checksum_size = None # filled in by create_hex_hash() + checksum_chars = uh.HEX_CHARS + + #: special for detecting if _hash_func is just a stub method. + supported = True + + #=================================================================== + # methods + #=================================================================== + @classmethod + def _norm_hash(cls, hash): + return hash.lower() + + def _calc_checksum(self, secret): + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + return str_to_uascii(self._hash_func(secret).hexdigest()) + + #=================================================================== + # eoc + #=================================================================== + +def create_hex_hash(digest, module=__name__, django_name=None, required=True): + """ + create hex-encoded unsalted hasher for specified digest algorithm. + + .. versionchanged:: 1.7.3 + If called with unknown/supported digest, won't throw error immediately, + but instead return a dummy hasher that will throw error when called. + + set ``required=True`` to restore old behavior. + """ + info = lookup_hash(digest, required=required) + name = "hex_" + info.name + if not info.supported: + info.digest_size = 0 + hasher = type(name, (HexDigestHash,), dict( + name=name, + __module__=module, # so ABCMeta won't clobber it + _hash_func=staticmethod(info.const), # sometimes it's a function, sometimes not. so wrap it. + checksum_size=info.digest_size*2, + __doc__="""This class implements a plain hexadecimal %s hash, and follows the :ref:`password-hash-api`. + +It supports no optional or contextual keywords. +""" % (info.name,) + )) + if not info.supported: + hasher.supported = False + if django_name: + hasher.django_name = django_name + return hasher + +#============================================================================= +# predefined handlers +#============================================================================= + +# NOTE: some digests below are marked as "required=False", because these may not be present on +# FIPS systems (see issue 116). if missing, will return stub hasher that throws error +# if an attempt is made to actually use hash/verify with them. + +hex_md4 = create_hex_hash("md4", required=False) +hex_md5 = create_hex_hash("md5", django_name="unsalted_md5", required=False) +hex_sha1 = create_hex_hash("sha1", required=False) +hex_sha256 = create_hex_hash("sha256") +hex_sha512 = create_hex_hash("sha512") + +#============================================================================= +# htdigest +#============================================================================= +class htdigest(uh.MinimalHandler): + """htdigest hash function. + + .. todo:: + document this hash + """ + name = "htdigest" + setting_kwds = () + context_kwds = ("user", "realm", "encoding") + default_encoding = "utf-8" + + @classmethod + def hash(cls, secret, user, realm, encoding=None): + # NOTE: this was deliberately written so that raw bytes are passed through + # unchanged, the encoding kwd is only used to handle unicode values. + if not encoding: + encoding = cls.default_encoding + uh.validate_secret(secret) + if isinstance(secret, unicode): + secret = secret.encode(encoding) + user = to_bytes(user, encoding, "user") + realm = to_bytes(realm, encoding, "realm") + data = render_bytes("%s:%s:%s", user, realm, secret) + return hashlib.md5(data).hexdigest() + + @classmethod + def _norm_hash(cls, hash): + """normalize hash to native string, and validate it""" + hash = to_native_str(hash, param="hash") + if len(hash) != 32: + raise uh.exc.MalformedHashError(cls, "wrong size") + for char in hash: + if char not in uh.LC_HEX_CHARS: + raise uh.exc.MalformedHashError(cls, "invalid chars in hash") + return hash + + @classmethod + def verify(cls, secret, hash, user, realm, encoding="utf-8"): + hash = cls._norm_hash(hash) + other = cls.hash(secret, user, realm, encoding) + return consteq(hash, other) + + @classmethod + def identify(cls, hash): + try: + cls._norm_hash(hash) + except ValueError: + return False + return True + + @uh.deprecated_method(deprecated="1.7", removed="2.0") + @classmethod + def genconfig(cls): + return cls.hash("", "", "") + + @uh.deprecated_method(deprecated="1.7", removed="2.0") + @classmethod + def genhash(cls, secret, config, user, realm, encoding=None): + # NOTE: 'config' is ignored, as this hash has no salting / other configuration. + # just have to make sure it's valid. + cls._norm_hash(config) + return cls.hash(secret, user, realm, encoding) + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/handlers/django.py b/ansible/lib/python3.11/site-packages/passlib/handlers/django.py new file mode 100644 index 000000000..6dd499ac2 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/handlers/django.py @@ -0,0 +1,512 @@ +"""passlib.handlers.django- Django password hash support""" +#============================================================================= +# imports +#============================================================================= +# core +from base64 import b64encode +from binascii import hexlify +from hashlib import md5, sha1, sha256 +import logging; log = logging.getLogger(__name__) +# site +# pkg +from passlib.handlers.bcrypt import _wrapped_bcrypt +from passlib.hash import argon2, bcrypt, pbkdf2_sha1, pbkdf2_sha256 +from passlib.utils import to_unicode, rng, getrandstr +from passlib.utils.binary import BASE64_CHARS +from passlib.utils.compat import str_to_uascii, uascii_to_str, unicode, u +from passlib.crypto.digest import pbkdf2_hmac +import passlib.utils.handlers as uh +# local +__all__ = [ + "django_salted_sha1", + "django_salted_md5", + "django_bcrypt", + "django_pbkdf2_sha1", + "django_pbkdf2_sha256", + "django_argon2", + "django_des_crypt", + "django_disabled", +] + +#============================================================================= +# lazy imports & constants +#============================================================================= + +# imported by django_des_crypt._calc_checksum() +des_crypt = None + +def _import_des_crypt(): + global des_crypt + if des_crypt is None: + from passlib.hash import des_crypt + return des_crypt + +# django 1.4's salt charset +SALT_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' + +#============================================================================= +# salted hashes +#============================================================================= +class DjangoSaltedHash(uh.HasSalt, uh.GenericHandler): + """base class providing common code for django hashes""" + # name, ident, checksum_size must be set by subclass. + # ident must include "$" suffix. + setting_kwds = ("salt", "salt_size") + + # NOTE: django 1.0-1.3 would accept empty salt strings. + # django 1.4 won't, but this appears to be regression + # (https://code.djangoproject.com/ticket/18144) + # so presumably it will be fixed in a later release. + default_salt_size = 12 + max_salt_size = None + salt_chars = SALT_CHARS + + checksum_chars = uh.LOWER_HEX_CHARS + + @classmethod + def from_string(cls, hash): + salt, chk = uh.parse_mc2(hash, cls.ident, handler=cls) + return cls(salt=salt, checksum=chk) + + def to_string(self): + return uh.render_mc2(self.ident, self.salt, self.checksum) + +# NOTE: only used by PBKDF2 +class DjangoVariableHash(uh.HasRounds, DjangoSaltedHash): + """base class providing common code for django hashes w/ variable rounds""" + setting_kwds = DjangoSaltedHash.setting_kwds + ("rounds",) + + min_rounds = 1 + + @classmethod + def from_string(cls, hash): + rounds, salt, chk = uh.parse_mc3(hash, cls.ident, handler=cls) + return cls(rounds=rounds, salt=salt, checksum=chk) + + def to_string(self): + return uh.render_mc3(self.ident, self.rounds, self.salt, self.checksum) + +class django_salted_sha1(DjangoSaltedHash): + """This class implements Django's Salted SHA1 hash, and follows the :ref:`password-hash-api`. + + It supports a variable-length salt, and uses a single round of SHA1. + + The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + + :type salt: str + :param salt: + Optional salt string. + If not specified, a 12 character one will be autogenerated (this is recommended). + If specified, may be any series of characters drawn from the regexp range ``[0-9a-zA-Z]``. + + :type salt_size: int + :param salt_size: + Optional number of characters to use when autogenerating new salts. + Defaults to 12, but can be any positive value. + + This should be compatible with Django 1.4's :class:`!SHA1PasswordHasher` class. + + .. versionchanged: 1.6 + This class now generates 12-character salts instead of 5, + and generated salts uses the character range ``[0-9a-zA-Z]`` instead of + the ``[0-9a-f]``. This is to be compatible with how Django >= 1.4 + generates these hashes; but hashes generated in this manner will still be + correctly interpreted by earlier versions of Django. + """ + name = "django_salted_sha1" + django_name = "sha1" + ident = u("sha1$") + checksum_size = 40 + + def _calc_checksum(self, secret): + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + return str_to_uascii(sha1(self.salt.encode("ascii") + secret).hexdigest()) + +class django_salted_md5(DjangoSaltedHash): + """This class implements Django's Salted MD5 hash, and follows the :ref:`password-hash-api`. + + It supports a variable-length salt, and uses a single round of MD5. + + The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + + :type salt: str + :param salt: + Optional salt string. + If not specified, a 12 character one will be autogenerated (this is recommended). + If specified, may be any series of characters drawn from the regexp range ``[0-9a-zA-Z]``. + + :type salt_size: int + :param salt_size: + Optional number of characters to use when autogenerating new salts. + Defaults to 12, but can be any positive value. + + This should be compatible with the hashes generated by + Django 1.4's :class:`!MD5PasswordHasher` class. + + .. versionchanged: 1.6 + This class now generates 12-character salts instead of 5, + and generated salts uses the character range ``[0-9a-zA-Z]`` instead of + the ``[0-9a-f]``. This is to be compatible with how Django >= 1.4 + generates these hashes; but hashes generated in this manner will still be + correctly interpreted by earlier versions of Django. + """ + name = "django_salted_md5" + django_name = "md5" + ident = u("md5$") + checksum_size = 32 + + def _calc_checksum(self, secret): + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + return str_to_uascii(md5(self.salt.encode("ascii") + secret).hexdigest()) + +#============================================================================= +# BCrypt +#============================================================================= + +django_bcrypt = uh.PrefixWrapper("django_bcrypt", bcrypt, + prefix=u('bcrypt$'), ident=u("bcrypt$"), + # NOTE: this docstring is duplicated in the docs, since sphinx + # seems to be having trouble reading it via autodata:: + doc="""This class implements Django 1.4's BCrypt wrapper, and follows the :ref:`password-hash-api`. + + This is identical to :class:`!bcrypt` itself, but with + the Django-specific prefix ``"bcrypt$"`` prepended. + + See :doc:`/lib/passlib.hash.bcrypt` for more details, + the usage and behavior is identical. + + This should be compatible with the hashes generated by + Django 1.4's :class:`!BCryptPasswordHasher` class. + + .. versionadded:: 1.6 + """) +django_bcrypt.django_name = "bcrypt" +django_bcrypt._using_clone_attrs += ("django_name",) + +#============================================================================= +# BCRYPT + SHA256 +#============================================================================= + +class django_bcrypt_sha256(_wrapped_bcrypt): + """This class implements Django 1.6's Bcrypt+SHA256 hash, and follows the :ref:`password-hash-api`. + + It supports a variable-length salt, and a variable number of rounds. + + While the algorithm and format is somewhat different, + the api and options for this hash are identical to :class:`!bcrypt` itself, + see :doc:`bcrypt ` for more details. + + .. versionadded:: 1.6.2 + """ + name = "django_bcrypt_sha256" + django_name = "bcrypt_sha256" + _digest = sha256 + + # sample hash: + # bcrypt_sha256$$2a$06$/3OeRpbOf8/l6nPPRdZPp.nRiyYqPobEZGdNRBWihQhiFDh1ws1tu + + # XXX: we can't use .ident attr due to bcrypt code using it. + # working around that via django_prefix + django_prefix = u('bcrypt_sha256$') + + @classmethod + def identify(cls, hash): + hash = uh.to_unicode_for_identify(hash) + if not hash: + return False + return hash.startswith(cls.django_prefix) + + @classmethod + def from_string(cls, hash): + hash = to_unicode(hash, "ascii", "hash") + if not hash.startswith(cls.django_prefix): + raise uh.exc.InvalidHashError(cls) + bhash = hash[len(cls.django_prefix):] + if not bhash.startswith("$2"): + raise uh.exc.MalformedHashError(cls) + return super(django_bcrypt_sha256, cls).from_string(bhash) + + def to_string(self): + bhash = super(django_bcrypt_sha256, self).to_string() + return uascii_to_str(self.django_prefix) + bhash + + def _calc_checksum(self, secret): + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + secret = hexlify(self._digest(secret).digest()) + return super(django_bcrypt_sha256, self)._calc_checksum(secret) + +#============================================================================= +# PBKDF2 variants +#============================================================================= + +class django_pbkdf2_sha256(DjangoVariableHash): + """This class implements Django's PBKDF2-HMAC-SHA256 hash, and follows the :ref:`password-hash-api`. + + It supports a variable-length salt, and a variable number of rounds. + + The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: + + :type salt: str + :param salt: + Optional salt string. + If not specified, a 12 character one will be autogenerated (this is recommended). + If specified, may be any series of characters drawn from the regexp range ``[0-9a-zA-Z]``. + + :type salt_size: int + :param salt_size: + Optional number of characters to use when autogenerating new salts. + Defaults to 12, but can be any positive value. + + :type rounds: int + :param rounds: + Optional number of rounds to use. + Defaults to 29000, but must be within ``range(1,1<<32)``. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include ``rounds`` + that are too small or too large, and ``salt`` strings that are too long. + + This should be compatible with the hashes generated by + Django 1.4's :class:`!PBKDF2PasswordHasher` class. + + .. versionadded:: 1.6 + """ + name = "django_pbkdf2_sha256" + django_name = "pbkdf2_sha256" + ident = u('pbkdf2_sha256$') + min_salt_size = 1 + max_rounds = 0xffffffff # setting at 32-bit limit for now + checksum_chars = uh.PADDED_BASE64_CHARS + checksum_size = 44 # 32 bytes -> base64 + default_rounds = pbkdf2_sha256.default_rounds # NOTE: django 1.6 uses 12000 + _digest = "sha256" + + def _calc_checksum(self, secret): + # NOTE: secret & salt will be encoded using UTF-8 by pbkdf2_hmac() + hash = pbkdf2_hmac(self._digest, secret, self.salt, self.rounds) + return b64encode(hash).rstrip().decode("ascii") + +class django_pbkdf2_sha1(django_pbkdf2_sha256): + """This class implements Django's PBKDF2-HMAC-SHA1 hash, and follows the :ref:`password-hash-api`. + + It supports a variable-length salt, and a variable number of rounds. + + The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: + + :type salt: str + :param salt: + Optional salt string. + If not specified, a 12 character one will be autogenerated (this is recommended). + If specified, may be any series of characters drawn from the regexp range ``[0-9a-zA-Z]``. + + :type salt_size: int + :param salt_size: + Optional number of characters to use when autogenerating new salts. + Defaults to 12, but can be any positive value. + + :type rounds: int + :param rounds: + Optional number of rounds to use. + Defaults to 131000, but must be within ``range(1,1<<32)``. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include ``rounds`` + that are too small or too large, and ``salt`` strings that are too long. + + This should be compatible with the hashes generated by + Django 1.4's :class:`!PBKDF2SHA1PasswordHasher` class. + + .. versionadded:: 1.6 + """ + name = "django_pbkdf2_sha1" + django_name = "pbkdf2_sha1" + ident = u('pbkdf2_sha1$') + checksum_size = 28 # 20 bytes -> base64 + default_rounds = pbkdf2_sha1.default_rounds # NOTE: django 1.6 uses 12000 + _digest = "sha1" + +#============================================================================= +# Argon2 +#============================================================================= + +# NOTE: as of 2019-11-11, Django's Argon2PasswordHasher only supports Type I; +# so limiting this to ensure that as well. + +django_argon2 = uh.PrefixWrapper( + name="django_argon2", + wrapped=argon2.using(type="I"), + prefix=u('argon2'), + ident=u('argon2$argon2i$'), + # NOTE: this docstring is duplicated in the docs, since sphinx + # seems to be having trouble reading it via autodata:: + doc="""This class implements Django 1.10's Argon2 wrapper, and follows the :ref:`password-hash-api`. + + This is identical to :class:`!argon2` itself, but with + the Django-specific prefix ``"argon2$"`` prepended. + + See :doc:`argon2 ` for more details, + the usage and behavior is identical. + + This should be compatible with the hashes generated by + Django 1.10's :class:`!Argon2PasswordHasher` class. + + .. versionadded:: 1.7 + """) +django_argon2.django_name = "argon2" +django_argon2._using_clone_attrs += ("django_name",) + +#============================================================================= +# DES +#============================================================================= +class django_des_crypt(uh.TruncateMixin, uh.HasSalt, uh.GenericHandler): + """This class implements Django's :class:`des_crypt` wrapper, and follows the :ref:`password-hash-api`. + + It supports a fixed-length salt. + + The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + + :type salt: str + :param salt: + Optional salt string. + If not specified, one will be autogenerated (this is recommended). + If specified, it must be 2 characters, drawn from the regexp range ``[./0-9A-Za-z]``. + + :param bool truncate_error: + By default, django_des_crypt will silently truncate passwords larger than 8 bytes. + Setting ``truncate_error=True`` will cause :meth:`~passlib.ifc.PasswordHash.hash` + to raise a :exc:`~passlib.exc.PasswordTruncateError` instead. + + .. versionadded:: 1.7 + + This should be compatible with the hashes generated by + Django 1.4's :class:`!CryptPasswordHasher` class. + Note that Django only supports this hash on Unix systems + (though :class:`!django_des_crypt` is available cross-platform + under Passlib). + + .. versionchanged:: 1.6 + This class will now accept hashes with empty salt strings, + since Django 1.4 generates them this way. + """ + name = "django_des_crypt" + django_name = "crypt" + setting_kwds = ("salt", "salt_size", "truncate_error") + ident = u("crypt$") + checksum_chars = salt_chars = uh.HASH64_CHARS + checksum_size = 11 + min_salt_size = default_salt_size = 2 + truncate_size = 8 + + # NOTE: regarding duplicate salt field: + # + # django 1.0 had a "crypt$$" hash format, + # used [a-z0-9] to generate a 5 char salt, stored it in salt1, + # duplicated the first two chars of salt1 as salt2. + # it would throw an error if salt1 was empty. + # + # django 1.4 started generating 2 char salt using the full alphabet, + # left salt1 empty, and only paid attention to salt2. + # + # in order to be compatible with django 1.0, the hashes generated + # by this function will always include salt1, unless the following + # class-level field is disabled (mainly used for testing) + use_duplicate_salt = True + + @classmethod + def from_string(cls, hash): + salt, chk = uh.parse_mc2(hash, cls.ident, handler=cls) + if chk: + # chk should be full des_crypt hash + if not salt: + # django 1.4 always uses empty salt field, + # so extract salt from des_crypt hash + salt = chk[:2] + elif salt[:2] != chk[:2]: + # django 1.0 stored 5 chars in salt field, and duplicated + # the first two chars in . we keep the full salt, + # but make sure the first two chars match as sanity check. + raise uh.exc.MalformedHashError(cls, + "first two digits of salt and checksum must match") + # in all cases, strip salt chars from + chk = chk[2:] + return cls(salt=salt, checksum=chk) + + def to_string(self): + salt = self.salt + chk = salt[:2] + self.checksum + if self.use_duplicate_salt: + # filling in salt field, so that we're compatible with django 1.0 + return uh.render_mc2(self.ident, salt, chk) + else: + # django 1.4+ style hash + return uh.render_mc2(self.ident, "", chk) + + def _calc_checksum(self, secret): + # NOTE: we lazily import des_crypt, + # since most django deploys won't use django_des_crypt + global des_crypt + if des_crypt is None: + _import_des_crypt() + # check for truncation (during .hash() calls only) + if self.use_defaults: + self._check_truncate_policy(secret) + return des_crypt(salt=self.salt[:2])._calc_checksum(secret) + +class django_disabled(uh.ifc.DisabledHash, uh.StaticHandler): + """This class provides disabled password behavior for Django, and follows the :ref:`password-hash-api`. + + This class does not implement a hash, but instead + claims the special hash string ``"!"`` which Django uses + to indicate an account's password has been disabled. + + * newly encrypted passwords will hash to ``"!"``. + * it rejects all passwords. + + .. note:: + + Django 1.6 prepends a randomly generated 40-char alphanumeric string + to each unusuable password. This class recognizes such strings, + but for backwards compatibility, still returns ``"!"``. + + See ``_ for why + Django appends an alphanumeric string. + + .. versionchanged:: 1.6.2 added Django 1.6 support + + .. versionchanged:: 1.7 started appending an alphanumeric string. + """ + name = "django_disabled" + _hash_prefix = u("!") + suffix_length = 40 + + # XXX: move this to StaticHandler, or wherever _hash_prefix is being used? + @classmethod + def identify(cls, hash): + hash = uh.to_unicode_for_identify(hash) + return hash.startswith(cls._hash_prefix) + + def _calc_checksum(self, secret): + # generate random suffix to match django's behavior + return getrandstr(rng, BASE64_CHARS[:-2], self.suffix_length) + + @classmethod + def verify(cls, secret, hash): + uh.validate_secret(secret) + if not cls.identify(hash): + raise uh.exc.InvalidHashError(cls) + return False + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/handlers/fshp.py b/ansible/lib/python3.11/site-packages/passlib/handlers/fshp.py new file mode 100644 index 000000000..db13e745b --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/handlers/fshp.py @@ -0,0 +1,214 @@ +"""passlib.handlers.fshp +""" + +#============================================================================= +# imports +#============================================================================= +# core +from base64 import b64encode, b64decode +import re +import logging; log = logging.getLogger(__name__) +# site +# pkg +from passlib.utils import to_unicode +import passlib.utils.handlers as uh +from passlib.utils.compat import bascii_to_str, iteritems, u,\ + unicode +from passlib.crypto.digest import pbkdf1 +# local +__all__ = [ + 'fshp', +] +#============================================================================= +# sha1-crypt +#============================================================================= +class fshp(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler): + """This class implements the FSHP password hash, and follows the :ref:`password-hash-api`. + + It supports a variable-length salt, and a variable number of rounds. + + The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: + + :param salt: + Optional raw salt string. + If not specified, one will be autogenerated (this is recommended). + + :param salt_size: + Optional number of bytes to use when autogenerating new salts. + Defaults to 16 bytes, but can be any non-negative value. + + :param rounds: + Optional number of rounds to use. + Defaults to 480000, must be between 1 and 4294967295, inclusive. + + :param variant: + Optionally specifies variant of FSHP to use. + + * ``0`` - uses SHA-1 digest (deprecated). + * ``1`` - uses SHA-2/256 digest (default). + * ``2`` - uses SHA-2/384 digest. + * ``3`` - uses SHA-2/512 digest. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include ``rounds`` + that are too small or too large, and ``salt`` strings that are too long. + + .. versionadded:: 1.6 + """ + + #=================================================================== + # class attrs + #=================================================================== + #--GenericHandler-- + name = "fshp" + setting_kwds = ("salt", "salt_size", "rounds", "variant") + checksum_chars = uh.PADDED_BASE64_CHARS + ident = u("{FSHP") + # checksum_size is property() that depends on variant + + #--HasRawSalt-- + default_salt_size = 16 # current passlib default, FSHP uses 8 + max_salt_size = None + + #--HasRounds-- + # FIXME: should probably use different default rounds + # based on the variant. setting for default variant (sha256) for now. + default_rounds = 480000 # current passlib default, FSHP uses 4096 + min_rounds = 1 # set by FSHP + max_rounds = 4294967295 # 32-bit integer limit - not set by FSHP + rounds_cost = "linear" + + #--variants-- + default_variant = 1 + _variant_info = { + # variant: (hash name, digest size) + 0: ("sha1", 20), + 1: ("sha256", 32), + 2: ("sha384", 48), + 3: ("sha512", 64), + } + _variant_aliases = dict( + [(unicode(k),k) for k in _variant_info] + + [(v[0],k) for k,v in iteritems(_variant_info)] + ) + + #=================================================================== + # configuration + #=================================================================== + @classmethod + def using(cls, variant=None, **kwds): + subcls = super(fshp, cls).using(**kwds) + if variant is not None: + subcls.default_variant = cls._norm_variant(variant) + return subcls + + #=================================================================== + # instance attrs + #=================================================================== + variant = None + + #=================================================================== + # init + #=================================================================== + def __init__(self, variant=None, **kwds): + # NOTE: variant must be set first, since it controls checksum size, etc. + self.use_defaults = kwds.get("use_defaults") # load this early + if variant is not None: + variant = self._norm_variant(variant) + elif self.use_defaults: + variant = self.default_variant + assert self._norm_variant(variant) == variant, "invalid default variant: %r" % (variant,) + else: + raise TypeError("no variant specified") + self.variant = variant + super(fshp, self).__init__(**kwds) + + @classmethod + def _norm_variant(cls, variant): + if isinstance(variant, bytes): + variant = variant.decode("ascii") + if isinstance(variant, unicode): + try: + variant = cls._variant_aliases[variant] + except KeyError: + raise ValueError("invalid fshp variant") + if not isinstance(variant, int): + raise TypeError("fshp variant must be int or known alias") + if variant not in cls._variant_info: + raise ValueError("invalid fshp variant") + return variant + + @property + def checksum_alg(self): + return self._variant_info[self.variant][0] + + @property + def checksum_size(self): + return self._variant_info[self.variant][1] + + #=================================================================== + # formatting + #=================================================================== + + _hash_regex = re.compile(u(r""" + ^ + \{FSHP + (\d+)\| # variant + (\d+)\| # salt size + (\d+)\} # rounds + ([a-zA-Z0-9+/]+={0,3}) # digest + $"""), re.X) + + @classmethod + def from_string(cls, hash): + hash = to_unicode(hash, "ascii", "hash") + m = cls._hash_regex.match(hash) + if not m: + raise uh.exc.InvalidHashError(cls) + variant, salt_size, rounds, data = m.group(1,2,3,4) + variant = int(variant) + salt_size = int(salt_size) + rounds = int(rounds) + try: + data = b64decode(data.encode("ascii")) + except TypeError: + raise uh.exc.MalformedHashError(cls) + salt = data[:salt_size] + chk = data[salt_size:] + return cls(salt=salt, checksum=chk, rounds=rounds, variant=variant) + + def to_string(self): + chk = self.checksum + salt = self.salt + data = bascii_to_str(b64encode(salt+chk)) + return "{FSHP%d|%d|%d}%s" % (self.variant, len(salt), self.rounds, data) + + #=================================================================== + # backend + #=================================================================== + + def _calc_checksum(self, secret): + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + # NOTE: for some reason, FSHP uses pbkdf1 with password & salt reversed. + # this has only a minimal impact on security, + # but it is worth noting this deviation. + return pbkdf1( + digest=self.checksum_alg, + secret=self.salt, + salt=secret, + rounds=self.rounds, + keylen=self.checksum_size, + ) + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/handlers/ldap_digests.py b/ansible/lib/python3.11/site-packages/passlib/handlers/ldap_digests.py new file mode 100644 index 000000000..30254f0b5 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/handlers/ldap_digests.py @@ -0,0 +1,359 @@ +"""passlib.handlers.digests - plain hash digests +""" +#============================================================================= +# imports +#============================================================================= +# core +from base64 import b64encode, b64decode +from hashlib import md5, sha1, sha256, sha512 +import logging; log = logging.getLogger(__name__) +import re +# site +# pkg +from passlib.handlers.misc import plaintext +from passlib.utils import unix_crypt_schemes, to_unicode +from passlib.utils.compat import uascii_to_str, unicode, u +from passlib.utils.decor import classproperty +import passlib.utils.handlers as uh +# local +__all__ = [ + "ldap_plaintext", + "ldap_md5", + "ldap_sha1", + "ldap_salted_md5", + "ldap_salted_sha1", + "ldap_salted_sha256", + "ldap_salted_sha512", + + ##"get_active_ldap_crypt_schemes", + "ldap_des_crypt", + "ldap_bsdi_crypt", + "ldap_md5_crypt", + "ldap_sha1_crypt", + "ldap_bcrypt", + "ldap_sha256_crypt", + "ldap_sha512_crypt", +] + +#============================================================================= +# ldap helpers +#============================================================================= +class _Base64DigestHelper(uh.StaticHandler): + """helper for ldap_md5 / ldap_sha1""" + # XXX: could combine this with hex digests in digests.py + + ident = None # required - prefix identifier + _hash_func = None # required - hash function + _hash_regex = None # required - regexp to recognize hash + checksum_chars = uh.PADDED_BASE64_CHARS + + @classproperty + def _hash_prefix(cls): + """tell StaticHandler to strip ident from checksum""" + return cls.ident + + def _calc_checksum(self, secret): + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + chk = self._hash_func(secret).digest() + return b64encode(chk).decode("ascii") + +class _SaltedBase64DigestHelper(uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler): + """helper for ldap_salted_md5 / ldap_salted_sha1""" + setting_kwds = ("salt", "salt_size") + checksum_chars = uh.PADDED_BASE64_CHARS + + ident = None # required - prefix identifier + _hash_func = None # required - hash function + _hash_regex = None # required - regexp to recognize hash + min_salt_size = max_salt_size = 4 + + # NOTE: openldap implementation uses 4 byte salt, + # but it's been reported (issue 30) that some servers use larger salts. + # the semi-related rfc3112 recommends support for up to 16 byte salts. + min_salt_size = 4 + default_salt_size = 4 + max_salt_size = 16 + + @classmethod + def from_string(cls, hash): + hash = to_unicode(hash, "ascii", "hash") + m = cls._hash_regex.match(hash) + if not m: + raise uh.exc.InvalidHashError(cls) + try: + data = b64decode(m.group("tmp").encode("ascii")) + except TypeError: + raise uh.exc.MalformedHashError(cls) + cs = cls.checksum_size + assert cs + return cls(checksum=data[:cs], salt=data[cs:]) + + def to_string(self): + data = self.checksum + self.salt + hash = self.ident + b64encode(data).decode("ascii") + return uascii_to_str(hash) + + def _calc_checksum(self, secret): + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + return self._hash_func(secret + self.salt).digest() + +#============================================================================= +# implementations +#============================================================================= +class ldap_md5(_Base64DigestHelper): + """This class stores passwords using LDAP's plain MD5 format, and follows the :ref:`password-hash-api`. + + The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods have no optional keywords. + """ + name = "ldap_md5" + ident = u("{MD5}") + _hash_func = md5 + _hash_regex = re.compile(u(r"^\{MD5\}(?P[+/a-zA-Z0-9]{22}==)$")) + +class ldap_sha1(_Base64DigestHelper): + """This class stores passwords using LDAP's plain SHA1 format, and follows the :ref:`password-hash-api`. + + The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods have no optional keywords. + """ + name = "ldap_sha1" + ident = u("{SHA}") + _hash_func = sha1 + _hash_regex = re.compile(u(r"^\{SHA\}(?P[+/a-zA-Z0-9]{27}=)$")) + +class ldap_salted_md5(_SaltedBase64DigestHelper): + """This class stores passwords using LDAP's salted MD5 format, and follows the :ref:`password-hash-api`. + + It supports a 4-16 byte salt. + + The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: + + :type salt: bytes + :param salt: + Optional salt string. + If not specified, one will be autogenerated (this is recommended). + If specified, it may be any 4-16 byte string. + + :type salt_size: int + :param salt_size: + Optional number of bytes to use when autogenerating new salts. + Defaults to 4 bytes for compatibility with the LDAP spec, + but some systems use larger salts, and Passlib supports + any value between 4-16. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include + ``salt`` strings that are too long. + + .. versionadded:: 1.6 + + .. versionchanged:: 1.6 + This format now supports variable length salts, instead of a fix 4 bytes. + """ + name = "ldap_salted_md5" + ident = u("{SMD5}") + checksum_size = 16 + _hash_func = md5 + _hash_regex = re.compile(u(r"^\{SMD5\}(?P[+/a-zA-Z0-9]{27,}={0,2})$")) + +class ldap_salted_sha1(_SaltedBase64DigestHelper): + """ + This class stores passwords using LDAP's "Salted SHA1" format, + and follows the :ref:`password-hash-api`. + + It supports a 4-16 byte salt. + + The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: + + :type salt: bytes + :param salt: + Optional salt string. + If not specified, one will be autogenerated (this is recommended). + If specified, it may be any 4-16 byte string. + + :type salt_size: int + :param salt_size: + Optional number of bytes to use when autogenerating new salts. + Defaults to 4 bytes for compatibility with the LDAP spec, + but some systems use larger salts, and Passlib supports + any value between 4-16. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include + ``salt`` strings that are too long. + + .. versionadded:: 1.6 + + .. versionchanged:: 1.6 + This format now supports variable length salts, instead of a fix 4 bytes. + """ + name = "ldap_salted_sha1" + ident = u("{SSHA}") + checksum_size = 20 + _hash_func = sha1 + # NOTE: 32 = ceil((20 + 4) * 4/3) + _hash_regex = re.compile(u(r"^\{SSHA\}(?P[+/a-zA-Z0-9]{32,}={0,2})$")) + + + +class ldap_salted_sha256(_SaltedBase64DigestHelper): + """ + This class stores passwords using LDAP's "Salted SHA2-256" format, + and follows the :ref:`password-hash-api`. + + It supports a 4-16 byte salt. + + The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: + + :type salt: bytes + :param salt: + Optional salt string. + If not specified, one will be autogenerated (this is recommended). + If specified, it may be any 4-16 byte string. + + :type salt_size: int + :param salt_size: + Optional number of bytes to use when autogenerating new salts. + Defaults to 8 bytes for compatibility with the LDAP spec, + but Passlib supports any value between 4-16. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include + ``salt`` strings that are too long. + + .. versionadded:: 1.7.3 + """ + name = "ldap_salted_sha256" + ident = u("{SSHA256}") + checksum_size = 32 + default_salt_size = 8 + _hash_func = sha256 + # NOTE: 48 = ceil((32 + 4) * 4/3) + _hash_regex = re.compile(u(r"^\{SSHA256\}(?P[+/a-zA-Z0-9]{48,}={0,2})$")) + + +class ldap_salted_sha512(_SaltedBase64DigestHelper): + """ + This class stores passwords using LDAP's "Salted SHA2-512" format, + and follows the :ref:`password-hash-api`. + + It supports a 4-16 byte salt. + + The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: + + :type salt: bytes + :param salt: + Optional salt string. + If not specified, one will be autogenerated (this is recommended). + If specified, it may be any 4-16 byte string. + + :type salt_size: int + :param salt_size: + Optional number of bytes to use when autogenerating new salts. + Defaults to 8 bytes for compatibility with the LDAP spec, + but Passlib supports any value between 4-16. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include + ``salt`` strings that are too long. + + .. versionadded:: 1.7.3 + """ + name = "ldap_salted_sha512" + ident = u("{SSHA512}") + checksum_size = 64 + default_salt_size = 8 + _hash_func = sha512 + # NOTE: 91 = ceil((64 + 4) * 4/3) + _hash_regex = re.compile(u(r"^\{SSHA512\}(?P[+/a-zA-Z0-9]{91,}={0,2})$")) + + +class ldap_plaintext(plaintext): + """This class stores passwords in plaintext, and follows the :ref:`password-hash-api`. + + This class acts much like the generic :class:`!passlib.hash.plaintext` handler, + except that it will identify a hash only if it does NOT begin with the ``{XXX}`` identifier prefix + used by RFC2307 passwords. + + The :meth:`~passlib.ifc.PasswordHash.hash`, :meth:`~passlib.ifc.PasswordHash.genhash`, and :meth:`~passlib.ifc.PasswordHash.verify` methods all require the + following additional contextual keyword: + + :type encoding: str + :param encoding: + This controls the character encoding to use (defaults to ``utf-8``). + + This encoding will be used to encode :class:`!unicode` passwords + under Python 2, and decode :class:`!bytes` hashes under Python 3. + + .. versionchanged:: 1.6 + The ``encoding`` keyword was added. + """ + # NOTE: this subclasses plaintext, since all it does differently + # is override identify() + + name = "ldap_plaintext" + _2307_pat = re.compile(u(r"^\{\w+\}.*$")) + + @uh.deprecated_method(deprecated="1.7", removed="2.0") + @classmethod + def genconfig(cls): + # Overridding plaintext.genconfig() since it returns "", + # but have to return non-empty value due to identify() below + return "!" + + @classmethod + def identify(cls, hash): + # NOTE: identifies all strings EXCEPT those with {XXX} prefix + hash = uh.to_unicode_for_identify(hash) + return bool(hash) and cls._2307_pat.match(hash) is None + +#============================================================================= +# {CRYPT} wrappers +# the following are wrappers around the base crypt algorithms, +# which add the ldap required {CRYPT} prefix +#============================================================================= +ldap_crypt_schemes = [ 'ldap_' + name for name in unix_crypt_schemes ] + +def _init_ldap_crypt_handlers(): + # NOTE: I don't like to implicitly modify globals() like this, + # but don't want to write out all these handlers out either :) + g = globals() + for wname in unix_crypt_schemes: + name = 'ldap_' + wname + g[name] = uh.PrefixWrapper(name, wname, prefix=u("{CRYPT}"), lazy=True) + del g +_init_ldap_crypt_handlers() + +##_lcn_host = None +##def get_host_ldap_crypt_schemes(): +## global _lcn_host +## if _lcn_host is None: +## from passlib.hosts import host_context +## schemes = host_context.schemes() +## _lcn_host = [ +## "ldap_" + name +## for name in unix_crypt_names +## if name in schemes +## ] +## return _lcn_host + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/handlers/md5_crypt.py b/ansible/lib/python3.11/site-packages/passlib/handlers/md5_crypt.py new file mode 100644 index 000000000..e3a2dfa49 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/handlers/md5_crypt.py @@ -0,0 +1,346 @@ +"""passlib.handlers.md5_crypt - md5-crypt algorithm""" +#============================================================================= +# imports +#============================================================================= +# core +from hashlib import md5 +import logging; log = logging.getLogger(__name__) +# site +# pkg +from passlib.utils import safe_crypt, test_crypt, repeat_string +from passlib.utils.binary import h64 +from passlib.utils.compat import unicode, u +import passlib.utils.handlers as uh +# local +__all__ = [ + "md5_crypt", + "apr_md5_crypt", +] + +#============================================================================= +# pure-python backend +#============================================================================= +_BNULL = b"\x00" +_MD5_MAGIC = b"$1$" +_APR_MAGIC = b"$apr1$" + +# pre-calculated offsets used to speed up C digest stage (see notes below). +# sequence generated using the following: + ##perms_order = "p,pp,ps,psp,sp,spp".split(",") + ##def offset(i): + ## key = (("p" if i % 2 else "") + ("s" if i % 3 else "") + + ## ("p" if i % 7 else "") + ("" if i % 2 else "p")) + ## return perms_order.index(key) + ##_c_digest_offsets = [(offset(i), offset(i+1)) for i in range(0,42,2)] +_c_digest_offsets = ( + (0, 3), (5, 1), (5, 3), (1, 2), (5, 1), (5, 3), (1, 3), + (4, 1), (5, 3), (1, 3), (5, 0), (5, 3), (1, 3), (5, 1), + (4, 3), (1, 3), (5, 1), (5, 2), (1, 3), (5, 1), (5, 3), + ) + +# map used to transpose bytes when encoding final digest +_transpose_map = (12, 6, 0, 13, 7, 1, 14, 8, 2, 15, 9, 3, 5, 10, 4, 11) + +def _raw_md5_crypt(pwd, salt, use_apr=False): + """perform raw md5-crypt calculation + + this function provides a pure-python implementation of the internals + for the MD5-Crypt algorithms; it doesn't handle any of the + parsing/validation of the hash strings themselves. + + :arg pwd: password chars/bytes to hash + :arg salt: salt chars to use + :arg use_apr: use apache variant + + :returns: + encoded checksum chars + """ + # NOTE: regarding 'apr' format: + # really, apache? you had to invent a whole new "$apr1$" format, + # when all you did was change the ident incorporated into the hash? + # would love to find webpage explaining why just using a portable + # implementation of $1$ wasn't sufficient. *nothing else* was changed. + + #=================================================================== + # init & validate inputs + #=================================================================== + + # validate secret + # XXX: not sure what official unicode policy is, using this as default + if isinstance(pwd, unicode): + pwd = pwd.encode("utf-8") + assert isinstance(pwd, bytes), "pwd not unicode or bytes" + if _BNULL in pwd: + raise uh.exc.NullPasswordError(md5_crypt) + pwd_len = len(pwd) + + # validate salt - should have been taken care of by caller + assert isinstance(salt, unicode), "salt not unicode" + salt = salt.encode("ascii") + assert len(salt) < 9, "salt too large" + # NOTE: spec says salts larger than 8 bytes should be truncated, + # instead of causing an error. this function assumes that's been + # taken care of by the handler class. + + # load APR specific constants + if use_apr: + magic = _APR_MAGIC + else: + magic = _MD5_MAGIC + + #=================================================================== + # digest B - used as subinput to digest A + #=================================================================== + db = md5(pwd + salt + pwd).digest() + + #=================================================================== + # digest A - used to initialize first round of digest C + #=================================================================== + # start out with pwd + magic + salt + a_ctx = md5(pwd + magic + salt) + a_ctx_update = a_ctx.update + + # add pwd_len bytes of b, repeating b as many times as needed. + a_ctx_update(repeat_string(db, pwd_len)) + + # add null chars & first char of password + # NOTE: this may have historically been a bug, + # where they meant to use db[0] instead of B_NULL, + # but the original code memclear'ed db, + # and now all implementations have to use this. + i = pwd_len + evenchar = pwd[:1] + while i: + a_ctx_update(_BNULL if i & 1 else evenchar) + i >>= 1 + + # finish A + da = a_ctx.digest() + + #=================================================================== + # digest C - for a 1000 rounds, combine A, S, and P + # digests in various ways; in order to burn CPU time. + #=================================================================== + + # NOTE: the original MD5-Crypt implementation performs the C digest + # calculation using the following loop: + # + ##dc = da + ##i = 0 + ##while i < rounds: + ## tmp_ctx = md5(pwd if i & 1 else dc) + ## if i % 3: + ## tmp_ctx.update(salt) + ## if i % 7: + ## tmp_ctx.update(pwd) + ## tmp_ctx.update(dc if i & 1 else pwd) + ## dc = tmp_ctx.digest() + ## i += 1 + # + # The code Passlib uses (below) implements an equivalent algorithm, + # it's just been heavily optimized to pre-calculate a large number + # of things beforehand. It works off of a couple of observations + # about the original algorithm: + # + # 1. each round is a combination of 'dc', 'salt', and 'pwd'; and the exact + # combination is determined by whether 'i' a multiple of 2,3, and/or 7. + # 2. since lcm(2,3,7)==42, the series of combinations will repeat + # every 42 rounds. + # 3. even rounds 0-40 consist of 'hash(dc + round-specific-constant)'; + # while odd rounds 1-41 consist of hash(round-specific-constant + dc) + # + # Using these observations, the following code... + # * calculates the round-specific combination of salt & pwd for each round 0-41 + # * runs through as many 42-round blocks as possible (23) + # * runs through as many pairs of rounds as needed for remaining rounds (17) + # * this results in the required 42*23+2*17=1000 rounds required by md5_crypt. + # + # this cuts out a lot of the control overhead incurred when running the + # original loop 1000 times in python, resulting in ~20% increase in + # speed under CPython (though still 2x slower than glibc crypt) + + # prepare the 6 combinations of pwd & salt which are needed + # (order of 'perms' must match how _c_digest_offsets was generated) + pwd_pwd = pwd+pwd + pwd_salt = pwd+salt + perms = [pwd, pwd_pwd, pwd_salt, pwd_salt+pwd, salt+pwd, salt+pwd_pwd] + + # build up list of even-round & odd-round constants, + # and store in 21-element list as (even,odd) pairs. + data = [ (perms[even], perms[odd]) for even, odd in _c_digest_offsets] + + # perform 23 blocks of 42 rounds each (for a total of 966 rounds) + dc = da + blocks = 23 + while blocks: + for even, odd in data: + dc = md5(odd + md5(dc + even).digest()).digest() + blocks -= 1 + + # perform 17 more pairs of rounds (34 more rounds, for a total of 1000) + for even, odd in data[:17]: + dc = md5(odd + md5(dc + even).digest()).digest() + + #=================================================================== + # encode digest using appropriate transpose map + #=================================================================== + return h64.encode_transposed_bytes(dc, _transpose_map).decode("ascii") + +#============================================================================= +# handler +#============================================================================= +class _MD5_Common(uh.HasSalt, uh.GenericHandler): + """common code for md5_crypt and apr_md5_crypt""" + #=================================================================== + # class attrs + #=================================================================== + # name - set in subclass + setting_kwds = ("salt", "salt_size") + # ident - set in subclass + checksum_size = 22 + checksum_chars = uh.HASH64_CHARS + + max_salt_size = 8 + salt_chars = uh.HASH64_CHARS + + #=================================================================== + # methods + #=================================================================== + + @classmethod + def from_string(cls, hash): + salt, chk = uh.parse_mc2(hash, cls.ident, handler=cls) + return cls(salt=salt, checksum=chk) + + def to_string(self): + return uh.render_mc2(self.ident, self.salt, self.checksum) + + # _calc_checksum() - provided by subclass + + #=================================================================== + # eoc + #=================================================================== + +class md5_crypt(uh.HasManyBackends, _MD5_Common): + """This class implements the MD5-Crypt password hash, and follows the :ref:`password-hash-api`. + + It supports a variable-length salt. + + The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: + + :type salt: str + :param salt: + Optional salt string. + If not specified, one will be autogenerated (this is recommended). + If specified, it must be 0-8 characters, drawn from the regexp range ``[./0-9A-Za-z]``. + + :type salt_size: int + :param salt_size: + Optional number of characters to use when autogenerating new salts. + Defaults to 8, but can be any value between 0 and 8. + (This is mainly needed when generating Cisco-compatible hashes, + which require ``salt_size=4``). + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include + ``salt`` strings that are too long. + + .. versionadded:: 1.6 + """ + #=================================================================== + # class attrs + #=================================================================== + name = "md5_crypt" + ident = u("$1$") + + #=================================================================== + # methods + #=================================================================== + # FIXME: can't find definitive policy on how md5-crypt handles non-ascii. + # all backends currently coerce -> utf-8 + + backends = ("os_crypt", "builtin") + + #--------------------------------------------------------------- + # os_crypt backend + #--------------------------------------------------------------- + @classmethod + def _load_backend_os_crypt(cls): + if test_crypt("test", '$1$test$pi/xDtU5WFVRqYS6BMU8X/'): + cls._set_calc_checksum_backend(cls._calc_checksum_os_crypt) + return True + else: + return False + + def _calc_checksum_os_crypt(self, secret): + config = self.ident + self.salt + hash = safe_crypt(secret, config) + if hash is None: + # py3's crypt.crypt() can't handle non-utf8 bytes. + # fallback to builtin alg, which is always available. + return self._calc_checksum_builtin(secret) + if not hash.startswith(config) or len(hash) != len(config) + 23: + raise uh.exc.CryptBackendError(self, config, hash) + return hash[-22:] + + #--------------------------------------------------------------- + # builtin backend + #--------------------------------------------------------------- + @classmethod + def _load_backend_builtin(cls): + cls._set_calc_checksum_backend(cls._calc_checksum_builtin) + return True + + def _calc_checksum_builtin(self, secret): + return _raw_md5_crypt(secret, self.salt) + + #=================================================================== + # eoc + #=================================================================== + +class apr_md5_crypt(_MD5_Common): + """This class implements the Apr-MD5-Crypt password hash, and follows the :ref:`password-hash-api`. + + It supports a variable-length salt. + + The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: + + :type salt: str + :param salt: + Optional salt string. + If not specified, one will be autogenerated (this is recommended). + If specified, it must be 0-8 characters, drawn from the regexp range ``[./0-9A-Za-z]``. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include + ``salt`` strings that are too long. + + .. versionadded:: 1.6 + """ + #=================================================================== + # class attrs + #=================================================================== + name = "apr_md5_crypt" + ident = u("$apr1$") + + #=================================================================== + # methods + #=================================================================== + def _calc_checksum(self, secret): + return _raw_md5_crypt(secret, self.salt, use_apr=True) + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/handlers/misc.py b/ansible/lib/python3.11/site-packages/passlib/handlers/misc.py new file mode 100644 index 000000000..44abc3438 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/handlers/misc.py @@ -0,0 +1,269 @@ +"""passlib.handlers.misc - misc generic handlers +""" +#============================================================================= +# imports +#============================================================================= +# core +import sys +import logging; log = logging.getLogger(__name__) +from warnings import warn +# site +# pkg +from passlib.utils import to_native_str, str_consteq +from passlib.utils.compat import unicode, u, unicode_or_bytes_types +import passlib.utils.handlers as uh +# local +__all__ = [ + "unix_disabled", + "unix_fallback", + "plaintext", +] + +#============================================================================= +# handler +#============================================================================= +class unix_fallback(uh.ifc.DisabledHash, uh.StaticHandler): + """This class provides the fallback behavior for unix shadow files, and follows the :ref:`password-hash-api`. + + This class does not implement a hash, but instead provides fallback + behavior as found in /etc/shadow on most unix variants. + If used, should be the last scheme in the context. + + * this class will positively identify all hash strings. + * for security, passwords will always hash to ``!``. + * it rejects all passwords if the hash is NOT an empty string (``!`` or ``*`` are frequently used). + * by default it rejects all passwords if the hash is an empty string, + but if ``enable_wildcard=True`` is passed to verify(), + all passwords will be allowed through if the hash is an empty string. + + .. deprecated:: 1.6 + This has been deprecated due to its "wildcard" feature, + and will be removed in Passlib 1.8. Use :class:`unix_disabled` instead. + """ + name = "unix_fallback" + context_kwds = ("enable_wildcard",) + + @classmethod + def identify(cls, hash): + if isinstance(hash, unicode_or_bytes_types): + return True + else: + raise uh.exc.ExpectedStringError(hash, "hash") + + def __init__(self, enable_wildcard=False, **kwds): + warn("'unix_fallback' is deprecated, " + "and will be removed in Passlib 1.8; " + "please use 'unix_disabled' instead.", + DeprecationWarning) + super(unix_fallback, self).__init__(**kwds) + self.enable_wildcard = enable_wildcard + + def _calc_checksum(self, secret): + if self.checksum: + # NOTE: hash will generally be "!", but we want to preserve + # it in case it's something else, like "*". + return self.checksum + else: + return u("!") + + @classmethod + def verify(cls, secret, hash, enable_wildcard=False): + uh.validate_secret(secret) + if not isinstance(hash, unicode_or_bytes_types): + raise uh.exc.ExpectedStringError(hash, "hash") + elif hash: + return False + else: + return enable_wildcard + +_MARKER_CHARS = u("*!") +_MARKER_BYTES = b"*!" + +class unix_disabled(uh.ifc.DisabledHash, uh.MinimalHandler): + """This class provides disabled password behavior for unix shadow files, + and follows the :ref:`password-hash-api`. + + This class does not implement a hash, but instead matches the "disabled account" + strings found in ``/etc/shadow`` on most Unix variants. "encrypting" a password + will simply return the disabled account marker. It will reject all passwords, + no matter the hash string. The :meth:`~passlib.ifc.PasswordHash.hash` + method supports one optional keyword: + + :type marker: str + :param marker: + Optional marker string which overrides the platform default + used to indicate a disabled account. + + If not specified, this will default to ``"*"`` on BSD systems, + and use the Linux default ``"!"`` for all other platforms. + (:attr:`!unix_disabled.default_marker` will contain the default value) + + .. versionadded:: 1.6 + This class was added as a replacement for the now-deprecated + :class:`unix_fallback` class, which had some undesirable features. + """ + name = "unix_disabled" + setting_kwds = ("marker",) + context_kwds = () + + _disable_prefixes = tuple(str(_MARKER_CHARS)) + + # TODO: rename attr to 'marker'... + if 'bsd' in sys.platform: # pragma: no cover -- runtime detection + default_marker = u("*") + else: + # use the linux default for other systems + # (glibc also supports adding old hash after the marker + # so it can be restored later). + default_marker = u("!") + + @classmethod + def using(cls, marker=None, **kwds): + subcls = super(unix_disabled, cls).using(**kwds) + if marker is not None: + if not cls.identify(marker): + raise ValueError("invalid marker: %r" % marker) + subcls.default_marker = marker + return subcls + + @classmethod + def identify(cls, hash): + # NOTE: technically, anything in the /etc/shadow password field + # which isn't valid crypt() output counts as "disabled". + # but that's rather ambiguous, and it's hard to predict what + # valid output is for unknown crypt() implementations. + # so to be on the safe side, we only match things *known* + # to be disabled field indicators, and will add others + # as they are found. things beginning w/ "$" should *never* match. + # + # things currently matched: + # * linux uses "!" + # * bsd uses "*" + # * linux may use "!" + hash to disable but preserve original hash + # * linux counts empty string as "any password"; + # this code recognizes it, but treats it the same as "!" + if isinstance(hash, unicode): + start = _MARKER_CHARS + elif isinstance(hash, bytes): + start = _MARKER_BYTES + else: + raise uh.exc.ExpectedStringError(hash, "hash") + return not hash or hash[0] in start + + @classmethod + def verify(cls, secret, hash): + uh.validate_secret(secret) + if not cls.identify(hash): # handles typecheck + raise uh.exc.InvalidHashError(cls) + return False + + @classmethod + def hash(cls, secret, **kwds): + if kwds: + uh.warn_hash_settings_deprecation(cls, kwds) + return cls.using(**kwds).hash(secret) + uh.validate_secret(secret) + marker = cls.default_marker + assert marker and cls.identify(marker) + return to_native_str(marker, param="marker") + + @uh.deprecated_method(deprecated="1.7", removed="2.0") + @classmethod + def genhash(cls, secret, config, marker=None): + if not cls.identify(config): + raise uh.exc.InvalidHashError(cls) + elif config: + # preserve the existing str,since it might contain a disabled password hash ("!" + hash) + uh.validate_secret(secret) + return to_native_str(config, param="config") + else: + if marker is not None: + cls = cls.using(marker=marker) + return cls.hash(secret) + + @classmethod + def disable(cls, hash=None): + out = cls.hash("") + if hash is not None: + hash = to_native_str(hash, param="hash") + if cls.identify(hash): + # extract original hash, so that we normalize marker + hash = cls.enable(hash) + if hash: + out += hash + return out + + @classmethod + def enable(cls, hash): + hash = to_native_str(hash, param="hash") + for prefix in cls._disable_prefixes: + if hash.startswith(prefix): + orig = hash[len(prefix):] + if orig: + return orig + else: + raise ValueError("cannot restore original hash") + raise uh.exc.InvalidHashError(cls) + +class plaintext(uh.MinimalHandler): + """This class stores passwords in plaintext, and follows the :ref:`password-hash-api`. + + The :meth:`~passlib.ifc.PasswordHash.hash`, :meth:`~passlib.ifc.PasswordHash.genhash`, and :meth:`~passlib.ifc.PasswordHash.verify` methods all require the + following additional contextual keyword: + + :type encoding: str + :param encoding: + This controls the character encoding to use (defaults to ``utf-8``). + + This encoding will be used to encode :class:`!unicode` passwords + under Python 2, and decode :class:`!bytes` hashes under Python 3. + + .. versionchanged:: 1.6 + The ``encoding`` keyword was added. + """ + # NOTE: this is subclassed by ldap_plaintext + + name = "plaintext" + setting_kwds = () + context_kwds = ("encoding",) + default_encoding = "utf-8" + + @classmethod + def identify(cls, hash): + if isinstance(hash, unicode_or_bytes_types): + return True + else: + raise uh.exc.ExpectedStringError(hash, "hash") + + @classmethod + def hash(cls, secret, encoding=None): + uh.validate_secret(secret) + if not encoding: + encoding = cls.default_encoding + return to_native_str(secret, encoding, "secret") + + @classmethod + def verify(cls, secret, hash, encoding=None): + if not encoding: + encoding = cls.default_encoding + hash = to_native_str(hash, encoding, "hash") + if not cls.identify(hash): + raise uh.exc.InvalidHashError(cls) + return str_consteq(cls.hash(secret, encoding), hash) + + @uh.deprecated_method(deprecated="1.7", removed="2.0") + @classmethod + def genconfig(cls): + return cls.hash("") + + @uh.deprecated_method(deprecated="1.7", removed="2.0") + @classmethod + def genhash(cls, secret, config, encoding=None): + # NOTE: 'config' is ignored, as this hash has no salting / etc + if not cls.identify(config): + raise uh.exc.InvalidHashError(cls) + return cls.hash(secret, encoding=encoding) + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/handlers/mssql.py b/ansible/lib/python3.11/site-packages/passlib/handlers/mssql.py new file mode 100644 index 000000000..b060b365c --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/handlers/mssql.py @@ -0,0 +1,244 @@ +"""passlib.handlers.mssql - MS-SQL Password Hash + +Notes +===== +MS-SQL has used a number of hash algs over the years, +most of which were exposed through the undocumented +'pwdencrypt' and 'pwdcompare' sql functions. + +Known formats +------------- +6.5 + snefru hash, ascii encoded password + no examples found + +7.0 + snefru hash, unicode (what encoding?) + saw ref that these blobs were 16 bytes in size + no examples found + +2000 + byte string using displayed as 0x hex, using 0x0100 prefix. + contains hashes of password and upper-case password. + +2007 + same as 2000, but without the upper-case hash. + +refs +---------- +https://blogs.msdn.com/b/lcris/archive/2007/04/30/sql-server-2005-about-login-password-hashes.aspx?Redirected=true +http://us.generation-nt.com/securing-passwords-hash-help-35429432.html +http://forum.md5decrypter.co.uk/topic230-mysql-and-mssql-get-password-hashes.aspx +http://www.theregister.co.uk/2002/07/08/cracking_ms_sql_server_passwords/ +""" +#============================================================================= +# imports +#============================================================================= +# core +from binascii import hexlify, unhexlify +from hashlib import sha1 +import re +import logging; log = logging.getLogger(__name__) +from warnings import warn +# site +# pkg +from passlib.utils import consteq +from passlib.utils.compat import bascii_to_str, unicode, u +import passlib.utils.handlers as uh +# local +__all__ = [ + "mssql2000", + "mssql2005", +] + +#============================================================================= +# mssql 2000 +#============================================================================= +def _raw_mssql(secret, salt): + assert isinstance(secret, unicode) + assert isinstance(salt, bytes) + return sha1(secret.encode("utf-16-le") + salt).digest() + +BIDENT = b"0x0100" +##BIDENT2 = b("\x01\x00") +UIDENT = u("0x0100") + +def _ident_mssql(hash, csize, bsize): + """common identify for mssql 2000/2005""" + if isinstance(hash, unicode): + if len(hash) == csize and hash.startswith(UIDENT): + return True + elif isinstance(hash, bytes): + if len(hash) == csize and hash.startswith(BIDENT): + return True + ##elif len(hash) == bsize and hash.startswith(BIDENT2): # raw bytes + ## return True + else: + raise uh.exc.ExpectedStringError(hash, "hash") + return False + +def _parse_mssql(hash, csize, bsize, handler): + """common parser for mssql 2000/2005; returns 4 byte salt + checksum""" + if isinstance(hash, unicode): + if len(hash) == csize and hash.startswith(UIDENT): + try: + return unhexlify(hash[6:].encode("utf-8")) + except TypeError: # throw when bad char found + pass + elif isinstance(hash, bytes): + # assumes ascii-compat encoding + assert isinstance(hash, bytes) + if len(hash) == csize and hash.startswith(BIDENT): + try: + return unhexlify(hash[6:]) + except TypeError: # throw when bad char found + pass + ##elif len(hash) == bsize and hash.startswith(BIDENT2): # raw bytes + ## return hash[2:] + else: + raise uh.exc.ExpectedStringError(hash, "hash") + raise uh.exc.InvalidHashError(handler) + +class mssql2000(uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler): + """This class implements the password hash used by MS-SQL 2000, and follows the :ref:`password-hash-api`. + + It supports a fixed-length salt. + + The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: + + :type salt: bytes + :param salt: + Optional salt string. + If not specified, one will be autogenerated (this is recommended). + If specified, it must be 4 bytes in length. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include + ``salt`` strings that are too long. + """ + #=================================================================== + # algorithm information + #=================================================================== + name = "mssql2000" + setting_kwds = ("salt",) + checksum_size = 40 + min_salt_size = max_salt_size = 4 + + #=================================================================== + # formatting + #=================================================================== + + # 0100 - 2 byte identifier + # 4 byte salt + # 20 byte checksum + # 20 byte checksum + # = 46 bytes + # encoded '0x' + 92 chars = 94 + + @classmethod + def identify(cls, hash): + return _ident_mssql(hash, 94, 46) + + @classmethod + def from_string(cls, hash): + data = _parse_mssql(hash, 94, 46, cls) + return cls(salt=data[:4], checksum=data[4:]) + + def to_string(self): + raw = self.salt + self.checksum + # raw bytes format - BIDENT2 + raw + return "0x0100" + bascii_to_str(hexlify(raw).upper()) + + def _calc_checksum(self, secret): + if isinstance(secret, bytes): + secret = secret.decode("utf-8") + salt = self.salt + return _raw_mssql(secret, salt) + _raw_mssql(secret.upper(), salt) + + @classmethod + def verify(cls, secret, hash): + # NOTE: we only compare against the upper-case hash + # XXX: add 'full' just to verify both checksums? + uh.validate_secret(secret) + self = cls.from_string(hash) + chk = self.checksum + if chk is None: + raise uh.exc.MissingDigestError(cls) + if isinstance(secret, bytes): + secret = secret.decode("utf-8") + result = _raw_mssql(secret.upper(), self.salt) + return consteq(result, chk[20:]) + +#============================================================================= +# handler +#============================================================================= +class mssql2005(uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler): + """This class implements the password hash used by MS-SQL 2005, and follows the :ref:`password-hash-api`. + + It supports a fixed-length salt. + + The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: + + :type salt: bytes + :param salt: + Optional salt string. + If not specified, one will be autogenerated (this is recommended). + If specified, it must be 4 bytes in length. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include + ``salt`` strings that are too long. + """ + #=================================================================== + # algorithm information + #=================================================================== + name = "mssql2005" + setting_kwds = ("salt",) + + checksum_size = 20 + min_salt_size = max_salt_size = 4 + + #=================================================================== + # formatting + #=================================================================== + + # 0x0100 - 2 byte identifier + # 4 byte salt + # 20 byte checksum + # = 26 bytes + # encoded '0x' + 52 chars = 54 + + @classmethod + def identify(cls, hash): + return _ident_mssql(hash, 54, 26) + + @classmethod + def from_string(cls, hash): + data = _parse_mssql(hash, 54, 26, cls) + return cls(salt=data[:4], checksum=data[4:]) + + def to_string(self): + raw = self.salt + self.checksum + # raw bytes format - BIDENT2 + raw + return "0x0100" + bascii_to_str(hexlify(raw)).upper() + + def _calc_checksum(self, secret): + if isinstance(secret, bytes): + secret = secret.decode("utf-8") + return _raw_mssql(secret, self.salt) + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/handlers/mysql.py b/ansible/lib/python3.11/site-packages/passlib/handlers/mysql.py new file mode 100644 index 000000000..4a7125350 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/handlers/mysql.py @@ -0,0 +1,128 @@ +"""passlib.handlers.mysql + +MySQL 3.2.3 / OLD_PASSWORD() + + This implements Mysql's OLD_PASSWORD algorithm, introduced in version 3.2.3, deprecated in version 4.1. + + See :mod:`passlib.handlers.mysql_41` for the new algorithm was put in place in version 4.1 + + This algorithm is known to be very insecure, and should only be used to verify existing password hashes. + + http://djangosnippets.org/snippets/1508/ + +MySQL 4.1.1 / NEW PASSWORD + This implements Mysql new PASSWORD algorithm, introduced in version 4.1. + + This function is unsalted, and therefore not very secure against rainbow attacks. + It should only be used when dealing with mysql passwords, + for all other purposes, you should use a salted hash function. + + Description taken from http://dev.mysql.com/doc/refman/6.0/en/password-hashing.html +""" +#============================================================================= +# imports +#============================================================================= +# core +from hashlib import sha1 +import re +import logging; log = logging.getLogger(__name__) +from warnings import warn +# site +# pkg +from passlib.utils import to_native_str +from passlib.utils.compat import bascii_to_str, unicode, u, \ + byte_elem_value, str_to_uascii +import passlib.utils.handlers as uh +# local +__all__ = [ + 'mysql323', + 'mysq41', +] + +#============================================================================= +# backend +#============================================================================= +class mysql323(uh.StaticHandler): + """This class implements the MySQL 3.2.3 password hash, and follows the :ref:`password-hash-api`. + + It has no salt and a single fixed round. + + The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept no optional keywords. + """ + #=================================================================== + # class attrs + #=================================================================== + name = "mysql323" + checksum_size = 16 + checksum_chars = uh.HEX_CHARS + + #=================================================================== + # methods + #=================================================================== + @classmethod + def _norm_hash(cls, hash): + return hash.lower() + + def _calc_checksum(self, secret): + # FIXME: no idea if mysql has a policy about handling unicode passwords + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + + MASK_32 = 0xffffffff + MASK_31 = 0x7fffffff + WHITE = b' \t' + + nr1 = 0x50305735 + nr2 = 0x12345671 + add = 7 + for c in secret: + if c in WHITE: + continue + tmp = byte_elem_value(c) + nr1 ^= ((((nr1 & 63)+add)*tmp) + (nr1 << 8)) & MASK_32 + nr2 = (nr2+((nr2 << 8) ^ nr1)) & MASK_32 + add = (add+tmp) & MASK_32 + return u("%08x%08x") % (nr1 & MASK_31, nr2 & MASK_31) + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# handler +#============================================================================= +class mysql41(uh.StaticHandler): + """This class implements the MySQL 4.1 password hash, and follows the :ref:`password-hash-api`. + + It has no salt and a single fixed round. + + The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept no optional keywords. + """ + #=================================================================== + # class attrs + #=================================================================== + name = "mysql41" + _hash_prefix = u("*") + checksum_chars = uh.HEX_CHARS + checksum_size = 40 + + #=================================================================== + # methods + #=================================================================== + @classmethod + def _norm_hash(cls, hash): + return hash.upper() + + def _calc_checksum(self, secret): + # FIXME: no idea if mysql has a policy about handling unicode passwords + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + return str_to_uascii(sha1(sha1(secret).digest()).hexdigest()).upper() + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/handlers/oracle.py b/ansible/lib/python3.11/site-packages/passlib/handlers/oracle.py new file mode 100644 index 000000000..a094f3721 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/handlers/oracle.py @@ -0,0 +1,172 @@ +"""passlib.handlers.oracle - Oracle DB Password Hashes""" +#============================================================================= +# imports +#============================================================================= +# core +from binascii import hexlify, unhexlify +from hashlib import sha1 +import re +import logging; log = logging.getLogger(__name__) +# site +# pkg +from passlib.utils import to_unicode, xor_bytes +from passlib.utils.compat import irange, u, \ + uascii_to_str, unicode, str_to_uascii +from passlib.crypto.des import des_encrypt_block +import passlib.utils.handlers as uh +# local +__all__ = [ + "oracle10g", + "oracle11g" +] + +#============================================================================= +# oracle10 +#============================================================================= +def des_cbc_encrypt(key, value, iv=b'\x00' * 8, pad=b'\x00'): + """performs des-cbc encryption, returns only last block. + + this performs a specific DES-CBC encryption implementation + as needed by the Oracle10 hash. it probably won't be useful for + other purposes as-is. + + input value is null-padded to multiple of 8 bytes. + + :arg key: des key as bytes + :arg value: value to encrypt, as bytes. + :param iv: optional IV + :param pad: optional pad byte + + :returns: last block of DES-CBC encryption of all ``value``'s byte blocks. + """ + value += pad * (-len(value) % 8) # null pad to multiple of 8 + hash = iv # start things off + for offset in irange(0,len(value),8): + chunk = xor_bytes(hash, value[offset:offset+8]) + hash = des_encrypt_block(key, chunk) + return hash + +# magic string used as initial des key by oracle10 +ORACLE10_MAGIC = b"\x01\x23\x45\x67\x89\xAB\xCD\xEF" + +class oracle10(uh.HasUserContext, uh.StaticHandler): + """This class implements the password hash used by Oracle up to version 10g, and follows the :ref:`password-hash-api`. + + It does a single round of hashing, and relies on the username as the salt. + + The :meth:`~passlib.ifc.PasswordHash.hash`, :meth:`~passlib.ifc.PasswordHash.genhash`, and :meth:`~passlib.ifc.PasswordHash.verify` methods all require the + following additional contextual keywords: + + :type user: str + :param user: name of oracle user account this password is associated with. + """ + #=================================================================== + # algorithm information + #=================================================================== + name = "oracle10" + checksum_chars = uh.HEX_CHARS + checksum_size = 16 + + #=================================================================== + # methods + #=================================================================== + @classmethod + def _norm_hash(cls, hash): + return hash.upper() + + def _calc_checksum(self, secret): + # FIXME: not sure how oracle handles unicode. + # online docs about 10g hash indicate it puts ascii chars + # in a 2-byte encoding w/ the high byte set to null. + # they don't say how it handles other chars, or what encoding. + # + # so for now, encoding secret & user to utf-16-be, + # since that fits, and if secret/user is bytes, + # we assume utf-8, and decode first. + # + # this whole mess really needs someone w/ an oracle system, + # and some answers :) + if isinstance(secret, bytes): + secret = secret.decode("utf-8") + user = to_unicode(self.user, "utf-8", param="user") + input = (user+secret).upper().encode("utf-16-be") + hash = des_cbc_encrypt(ORACLE10_MAGIC, input) + hash = des_cbc_encrypt(hash, input) + return hexlify(hash).decode("ascii").upper() + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# oracle11 +#============================================================================= +class oracle11(uh.HasSalt, uh.GenericHandler): + """This class implements the Oracle11g password hash, and follows the :ref:`password-hash-api`. + + It supports a fixed-length salt. + + The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: + + :type salt: str + :param salt: + Optional salt string. + If not specified, one will be autogenerated (this is recommended). + If specified, it must be 20 hexadecimal characters. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include + ``salt`` strings that are too long. + + .. versionadded:: 1.6 + """ + #=================================================================== + # class attrs + #=================================================================== + #--GenericHandler-- + name = "oracle11" + setting_kwds = ("salt",) + checksum_size = 40 + checksum_chars = uh.UPPER_HEX_CHARS + + #--HasSalt-- + min_salt_size = max_salt_size = 20 + salt_chars = uh.UPPER_HEX_CHARS + + + #=================================================================== + # methods + #=================================================================== + _hash_regex = re.compile(u("^S:(?P[0-9a-f]{40})(?P[0-9a-f]{20})$"), re.I) + + @classmethod + def from_string(cls, hash): + hash = to_unicode(hash, "ascii", "hash") + m = cls._hash_regex.match(hash) + if not m: + raise uh.exc.InvalidHashError(cls) + salt, chk = m.group("salt", "chk") + return cls(salt=salt, checksum=chk.upper()) + + def to_string(self): + chk = self.checksum + hash = u("S:%s%s") % (chk.upper(), self.salt.upper()) + return uascii_to_str(hash) + + def _calc_checksum(self, secret): + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + chk = sha1(secret + unhexlify(self.salt.encode("ascii"))).hexdigest() + return str_to_uascii(chk).upper() + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/handlers/pbkdf2.py b/ansible/lib/python3.11/site-packages/passlib/handlers/pbkdf2.py new file mode 100644 index 000000000..274278d86 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/handlers/pbkdf2.py @@ -0,0 +1,475 @@ +"""passlib.handlers.pbkdf - PBKDF2 based hashes""" +#============================================================================= +# imports +#============================================================================= +# core +from binascii import hexlify, unhexlify +from base64 import b64encode, b64decode +import logging; log = logging.getLogger(__name__) +# site +# pkg +from passlib.utils import to_unicode +from passlib.utils.binary import ab64_decode, ab64_encode +from passlib.utils.compat import str_to_bascii, u, uascii_to_str, unicode +from passlib.crypto.digest import pbkdf2_hmac +import passlib.utils.handlers as uh +# local +__all__ = [ + "pbkdf2_sha1", + "pbkdf2_sha256", + "pbkdf2_sha512", + "cta_pbkdf2_sha1", + "dlitz_pbkdf2_sha1", + "grub_pbkdf2_sha512", +] + +#============================================================================= +# +#============================================================================= +class Pbkdf2DigestHandler(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler): + """base class for various pbkdf2_{digest} algorithms""" + #=================================================================== + # class attrs + #=================================================================== + + #--GenericHandler-- + setting_kwds = ("salt", "salt_size", "rounds") + checksum_chars = uh.HASH64_CHARS + + #--HasSalt-- + default_salt_size = 16 + max_salt_size = 1024 + + #--HasRounds-- + default_rounds = None # set by subclass + min_rounds = 1 + max_rounds = 0xffffffff # setting at 32-bit limit for now + rounds_cost = "linear" + + #--this class-- + _digest = None # name of subclass-specified hash + + # NOTE: max_salt_size and max_rounds are arbitrarily chosen to provide sanity check. + # the underlying pbkdf2 specifies no bounds for either. + + # NOTE: defaults chosen to be at least as large as pbkdf2 rfc recommends... + # >8 bytes of entropy in salt, >1000 rounds + # increased due to time since rfc established + + #=================================================================== + # methods + #=================================================================== + + @classmethod + def from_string(cls, hash): + rounds, salt, chk = uh.parse_mc3(hash, cls.ident, handler=cls) + salt = ab64_decode(salt.encode("ascii")) + if chk: + chk = ab64_decode(chk.encode("ascii")) + return cls(rounds=rounds, salt=salt, checksum=chk) + + def to_string(self): + salt = ab64_encode(self.salt).decode("ascii") + chk = ab64_encode(self.checksum).decode("ascii") + return uh.render_mc3(self.ident, self.rounds, salt, chk) + + def _calc_checksum(self, secret): + # NOTE: pbkdf2_hmac() will encode secret & salt using UTF8 + return pbkdf2_hmac(self._digest, secret, self.salt, self.rounds, self.checksum_size) + +def create_pbkdf2_hash(hash_name, digest_size, rounds=12000, ident=None, module=__name__): + """create new Pbkdf2DigestHandler subclass for a specific hash""" + name = 'pbkdf2_' + hash_name + if ident is None: + ident = u("$pbkdf2-%s$") % (hash_name,) + base = Pbkdf2DigestHandler + return type(name, (base,), dict( + __module__=module, # so ABCMeta won't clobber it. + name=name, + ident=ident, + _digest = hash_name, + default_rounds=rounds, + checksum_size=digest_size, + encoded_checksum_size=(digest_size*4+2)//3, + __doc__="""This class implements a generic ``PBKDF2-HMAC-%(digest)s``-based password hash, and follows the :ref:`password-hash-api`. + + It supports a variable-length salt, and a variable number of rounds. + + The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: + + :type salt: bytes + :param salt: + Optional salt bytes. + If specified, the length must be between 0-1024 bytes. + If not specified, a %(dsc)d byte salt will be autogenerated (this is recommended). + + :type salt_size: int + :param salt_size: + Optional number of bytes to use when autogenerating new salts. + Defaults to %(dsc)d bytes, but can be any value between 0 and 1024. + + :type rounds: int + :param rounds: + Optional number of rounds to use. + Defaults to %(dr)d, but must be within ``range(1,1<<32)``. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include ``rounds`` + that are too small or too large, and ``salt`` strings that are too long. + + .. versionadded:: 1.6 + """ % dict(digest=hash_name.upper(), dsc=base.default_salt_size, dr=rounds) + )) + +#------------------------------------------------------------------------ +# derived handlers +#------------------------------------------------------------------------ +pbkdf2_sha1 = create_pbkdf2_hash("sha1", 20, 131000, ident=u("$pbkdf2$")) +pbkdf2_sha256 = create_pbkdf2_hash("sha256", 32, 29000) +pbkdf2_sha512 = create_pbkdf2_hash("sha512", 64, 25000) + +ldap_pbkdf2_sha1 = uh.PrefixWrapper("ldap_pbkdf2_sha1", pbkdf2_sha1, "{PBKDF2}", "$pbkdf2$", ident=True) +ldap_pbkdf2_sha256 = uh.PrefixWrapper("ldap_pbkdf2_sha256", pbkdf2_sha256, "{PBKDF2-SHA256}", "$pbkdf2-sha256$", ident=True) +ldap_pbkdf2_sha512 = uh.PrefixWrapper("ldap_pbkdf2_sha512", pbkdf2_sha512, "{PBKDF2-SHA512}", "$pbkdf2-sha512$", ident=True) + +#============================================================================= +# cryptacular's pbkdf2 hash +#============================================================================= + +# bytes used by cta hash for base64 values 63 & 64 +CTA_ALTCHARS = b"-_" + +class cta_pbkdf2_sha1(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler): + """This class implements Cryptacular's PBKDF2-based crypt algorithm, and follows the :ref:`password-hash-api`. + + It supports a variable-length salt, and a variable number of rounds. + + The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: + + :type salt: bytes + :param salt: + Optional salt bytes. + If specified, it may be any length. + If not specified, a one will be autogenerated (this is recommended). + + :type salt_size: int + :param salt_size: + Optional number of bytes to use when autogenerating new salts. + Defaults to 16 bytes, but can be any value between 0 and 1024. + + :type rounds: int + :param rounds: + Optional number of rounds to use. + Defaults to 60000, must be within ``range(1,1<<32)``. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include ``rounds`` + that are too small or too large, and ``salt`` strings that are too long. + + .. versionadded:: 1.6 + """ + + #=================================================================== + # class attrs + #=================================================================== + #--GenericHandler-- + name = "cta_pbkdf2_sha1" + setting_kwds = ("salt", "salt_size", "rounds") + ident = u("$p5k2$") + checksum_size = 20 + + # NOTE: max_salt_size and max_rounds are arbitrarily chosen to provide a + # sanity check. underlying algorithm (and reference implementation) + # allows effectively unbounded values for both of these parameters. + + #--HasSalt-- + default_salt_size = 16 + max_salt_size = 1024 + + #--HasRounds-- + default_rounds = pbkdf2_sha1.default_rounds + min_rounds = 1 + max_rounds = 0xffffffff # setting at 32-bit limit for now + rounds_cost = "linear" + + #=================================================================== + # formatting + #=================================================================== + + # hash $p5k2$1000$ZxK4ZBJCfQg=$jJZVscWtO--p1-xIZl6jhO2LKR0= + # ident $p5k2$ + # rounds 1000 + # salt ZxK4ZBJCfQg= + # chk jJZVscWtO--p1-xIZl6jhO2LKR0= + # NOTE: rounds in hex + + @classmethod + def from_string(cls, hash): + # NOTE: passlib deviation - forbidding zero-padded rounds + rounds, salt, chk = uh.parse_mc3(hash, cls.ident, rounds_base=16, handler=cls) + salt = b64decode(salt.encode("ascii"), CTA_ALTCHARS) + if chk: + chk = b64decode(chk.encode("ascii"), CTA_ALTCHARS) + return cls(rounds=rounds, salt=salt, checksum=chk) + + def to_string(self): + salt = b64encode(self.salt, CTA_ALTCHARS).decode("ascii") + chk = b64encode(self.checksum, CTA_ALTCHARS).decode("ascii") + return uh.render_mc3(self.ident, self.rounds, salt, chk, rounds_base=16) + + #=================================================================== + # backend + #=================================================================== + def _calc_checksum(self, secret): + # NOTE: pbkdf2_hmac() will encode secret & salt using utf-8 + return pbkdf2_hmac("sha1", secret, self.salt, self.rounds, 20) + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# dlitz's pbkdf2 hash +#============================================================================= +class dlitz_pbkdf2_sha1(uh.HasRounds, uh.HasSalt, uh.GenericHandler): + """This class implements Dwayne Litzenberger's PBKDF2-based crypt algorithm, and follows the :ref:`password-hash-api`. + + It supports a variable-length salt, and a variable number of rounds. + + The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: + + :type salt: str + :param salt: + Optional salt string. + If specified, it may be any length, but must use the characters in the regexp range ``[./0-9A-Za-z]``. + If not specified, a 16 character salt will be autogenerated (this is recommended). + + :type salt_size: int + :param salt_size: + Optional number of bytes to use when autogenerating new salts. + Defaults to 16 bytes, but can be any value between 0 and 1024. + + :type rounds: int + :param rounds: + Optional number of rounds to use. + Defaults to 60000, must be within ``range(1,1<<32)``. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include ``rounds`` + that are too small or too large, and ``salt`` strings that are too long. + + .. versionadded:: 1.6 + """ + + #=================================================================== + # class attrs + #=================================================================== + #--GenericHandler-- + name = "dlitz_pbkdf2_sha1" + setting_kwds = ("salt", "salt_size", "rounds") + ident = u("$p5k2$") + _stub_checksum = u("0" * 48 + "=") + + # NOTE: max_salt_size and max_rounds are arbitrarily chosen to provide a + # sanity check. underlying algorithm (and reference implementation) + # allows effectively unbounded values for both of these parameters. + + #--HasSalt-- + default_salt_size = 16 + max_salt_size = 1024 + salt_chars = uh.HASH64_CHARS + + #--HasRounds-- + # NOTE: for security, the default here is set to match pbkdf2_sha1, + # even though this hash's extra block makes it twice as slow. + default_rounds = pbkdf2_sha1.default_rounds + min_rounds = 1 + max_rounds = 0xffffffff # setting at 32-bit limit for now + rounds_cost = "linear" + + #=================================================================== + # formatting + #=================================================================== + + # hash $p5k2$c$u9HvcT4d$Sd1gwSVCLZYAuqZ25piRnbBEoAesaa/g + # ident $p5k2$ + # rounds c + # salt u9HvcT4d + # chk Sd1gwSVCLZYAuqZ25piRnbBEoAesaa/g + # rounds in lowercase hex, no zero padding + + @classmethod + def from_string(cls, hash): + rounds, salt, chk = uh.parse_mc3(hash, cls.ident, rounds_base=16, + default_rounds=400, handler=cls) + return cls(rounds=rounds, salt=salt, checksum=chk) + + def to_string(self): + rounds = self.rounds + if rounds == 400: + rounds = None # omit rounds measurement if == 400 + return uh.render_mc3(self.ident, rounds, self.salt, self.checksum, rounds_base=16) + + def _get_config(self): + rounds = self.rounds + if rounds == 400: + rounds = None # omit rounds measurement if == 400 + return uh.render_mc3(self.ident, rounds, self.salt, None, rounds_base=16) + + #=================================================================== + # backend + #=================================================================== + def _calc_checksum(self, secret): + # NOTE: pbkdf2_hmac() will encode secret & salt using utf-8 + salt = self._get_config() + result = pbkdf2_hmac("sha1", secret, salt, self.rounds, 24) + return ab64_encode(result).decode("ascii") + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# crowd +#============================================================================= +class atlassian_pbkdf2_sha1(uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler): + """This class implements the PBKDF2 hash used by Atlassian. + + It supports a fixed-length salt, and a fixed number of rounds. + + The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: + + :type salt: bytes + :param salt: + Optional salt bytes. + If specified, the length must be exactly 16 bytes. + If not specified, a salt will be autogenerated (this is recommended). + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include + ``salt`` strings that are too long. + + .. versionadded:: 1.6 + """ + #--GenericHandler-- + name = "atlassian_pbkdf2_sha1" + setting_kwds =("salt",) + ident = u("{PKCS5S2}") + checksum_size = 32 + + #--HasRawSalt-- + min_salt_size = max_salt_size = 16 + + @classmethod + def from_string(cls, hash): + hash = to_unicode(hash, "ascii", "hash") + ident = cls.ident + if not hash.startswith(ident): + raise uh.exc.InvalidHashError(cls) + data = b64decode(hash[len(ident):].encode("ascii")) + salt, chk = data[:16], data[16:] + return cls(salt=salt, checksum=chk) + + def to_string(self): + data = self.salt + self.checksum + hash = self.ident + b64encode(data).decode("ascii") + return uascii_to_str(hash) + + def _calc_checksum(self, secret): + # TODO: find out what crowd's policy is re: unicode + # crowd seems to use a fixed number of rounds. + # NOTE: pbkdf2_hmac() will encode secret & salt using utf-8 + return pbkdf2_hmac("sha1", secret, self.salt, 10000, 32) + +#============================================================================= +# grub +#============================================================================= +class grub_pbkdf2_sha512(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler): + """This class implements Grub's pbkdf2-hmac-sha512 hash, and follows the :ref:`password-hash-api`. + + It supports a variable-length salt, and a variable number of rounds. + + The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: + + :type salt: bytes + :param salt: + Optional salt bytes. + If specified, the length must be between 0-1024 bytes. + If not specified, a 64 byte salt will be autogenerated (this is recommended). + + :type salt_size: int + :param salt_size: + Optional number of bytes to use when autogenerating new salts. + Defaults to 64 bytes, but can be any value between 0 and 1024. + + :type rounds: int + :param rounds: + Optional number of rounds to use. + Defaults to 19000, but must be within ``range(1,1<<32)``. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include ``rounds`` + that are too small or too large, and ``salt`` strings that are too long. + + .. versionadded:: 1.6 + """ + name = "grub_pbkdf2_sha512" + setting_kwds = ("salt", "salt_size", "rounds") + + ident = u("grub.pbkdf2.sha512.") + checksum_size = 64 + + # NOTE: max_salt_size and max_rounds are arbitrarily chosen to provide a + # sanity check. the underlying pbkdf2 specifies no bounds for either, + # and it's not clear what grub specifies. + + default_salt_size = 64 + max_salt_size = 1024 + + default_rounds = pbkdf2_sha512.default_rounds + min_rounds = 1 + max_rounds = 0xffffffff # setting at 32-bit limit for now + rounds_cost = "linear" + + @classmethod + def from_string(cls, hash): + rounds, salt, chk = uh.parse_mc3(hash, cls.ident, sep=u("."), + handler=cls) + salt = unhexlify(salt.encode("ascii")) + if chk: + chk = unhexlify(chk.encode("ascii")) + return cls(rounds=rounds, salt=salt, checksum=chk) + + def to_string(self): + salt = hexlify(self.salt).decode("ascii").upper() + chk = hexlify(self.checksum).decode("ascii").upper() + return uh.render_mc3(self.ident, self.rounds, salt, chk, sep=u(".")) + + def _calc_checksum(self, secret): + # TODO: find out what grub's policy is re: unicode + # NOTE: pbkdf2_hmac() will encode secret & salt using utf-8 + return pbkdf2_hmac("sha512", secret, self.salt, self.rounds, 64) + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/handlers/phpass.py b/ansible/lib/python3.11/site-packages/passlib/handlers/phpass.py new file mode 100644 index 000000000..6736f0f2d --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/handlers/phpass.py @@ -0,0 +1,135 @@ +"""passlib.handlers.phpass - PHPass Portable Crypt + +phppass located - http://www.openwall.com/phpass/ +algorithm described - http://www.openwall.com/articles/PHP-Users-Passwords + +phpass context - blowfish, bsdi_crypt, phpass +""" +#============================================================================= +# imports +#============================================================================= +# core +from hashlib import md5 +import logging; log = logging.getLogger(__name__) +# site +# pkg +from passlib.utils.binary import h64 +from passlib.utils.compat import u, uascii_to_str, unicode +import passlib.utils.handlers as uh +# local +__all__ = [ + "phpass", +] + +#============================================================================= +# phpass +#============================================================================= +class phpass(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.GenericHandler): + """This class implements the PHPass Portable Hash, and follows the :ref:`password-hash-api`. + + It supports a fixed-length salt, and a variable number of rounds. + + The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: + + :type salt: str + :param salt: + Optional salt string. + If not specified, one will be autogenerated (this is recommended). + If specified, it must be 8 characters, drawn from the regexp range ``[./0-9A-Za-z]``. + + :type rounds: int + :param rounds: + Optional number of rounds to use. + Defaults to 19, must be between 7 and 30, inclusive. + This value is logarithmic, the actual number of iterations used will be :samp:`2**{rounds}`. + + :type ident: str + :param ident: + phpBB3 uses ``H`` instead of ``P`` for its identifier, + this may be set to ``H`` in order to generate phpBB3 compatible hashes. + it defaults to ``P``. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include ``rounds`` + that are too small or too large, and ``salt`` strings that are too long. + + .. versionadded:: 1.6 + """ + + #=================================================================== + # class attrs + #=================================================================== + #--GenericHandler-- + name = "phpass" + setting_kwds = ("salt", "rounds", "ident") + checksum_chars = uh.HASH64_CHARS + + #--HasSalt-- + min_salt_size = max_salt_size = 8 + salt_chars = uh.HASH64_CHARS + + #--HasRounds-- + default_rounds = 19 + min_rounds = 7 + max_rounds = 30 + rounds_cost = "log2" + + #--HasManyIdents-- + default_ident = u("$P$") + ident_values = (u("$P$"), u("$H$")) + ident_aliases = {u("P"):u("$P$"), u("H"):u("$H$")} + + #=================================================================== + # formatting + #=================================================================== + + #$P$9IQRaTwmfeRo7ud9Fh4E2PdI0S3r.L0 + # $P$ + # 9 + # IQRaTwmf + # eRo7ud9Fh4E2PdI0S3r.L0 + + @classmethod + def from_string(cls, hash): + ident, data = cls._parse_ident(hash) + rounds, salt, chk = data[0], data[1:9], data[9:] + return cls( + ident=ident, + rounds=h64.decode_int6(rounds.encode("ascii")), + salt=salt, + checksum=chk or None, + ) + + def to_string(self): + hash = u("%s%s%s%s") % (self.ident, + h64.encode_int6(self.rounds).decode("ascii"), + self.salt, + self.checksum or u('')) + return uascii_to_str(hash) + + #=================================================================== + # backend + #=================================================================== + def _calc_checksum(self, secret): + # FIXME: can't find definitive policy on how phpass handles non-ascii. + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + real_rounds = 1<`_ + hash names. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include ``rounds`` + that are too small or too large, and ``salt`` strings that are too long. + + .. versionadded:: 1.6 + + In addition to the standard :ref:`password-hash-api` methods, + this class also provides the following methods for manipulating Passlib + scram hashes in ways useful for pluging into a SCRAM protocol stack: + + .. automethod:: extract_digest_info + .. automethod:: extract_digest_algs + .. automethod:: derive_digest + """ + #=================================================================== + # class attrs + #=================================================================== + + # NOTE: unlike most GenericHandler classes, the 'checksum' attr of + # ScramHandler is actually a map from digest_name -> digest, so + # many of the standard methods have been overridden. + + # NOTE: max_salt_size and max_rounds are arbitrarily chosen to provide + # a sanity check; the underlying pbkdf2 specifies no bounds for either. + + #--GenericHandler-- + name = "scram" + setting_kwds = ("salt", "salt_size", "rounds", "algs") + ident = u("$scram$") + + #--HasSalt-- + default_salt_size = 12 + max_salt_size = 1024 + + #--HasRounds-- + default_rounds = 100000 + min_rounds = 1 + max_rounds = 2**32-1 + rounds_cost = "linear" + + #--custom-- + + # default algorithms when creating new hashes. + default_algs = ["sha-1", "sha-256", "sha-512"] + + # list of algs verify prefers to use, in order. + _verify_algs = ["sha-256", "sha-512", "sha-224", "sha-384", "sha-1"] + + #=================================================================== + # instance attrs + #=================================================================== + + # 'checksum' is different from most GenericHandler subclasses, + # in that it contains a dict mapping from alg -> digest, + # or None if no checksum present. + + # list of algorithms to create/compare digests for. + algs = None + + #=================================================================== + # scram frontend helpers + #=================================================================== + @classmethod + def extract_digest_info(cls, hash, alg): + """return (salt, rounds, digest) for specific hash algorithm. + + :type hash: str + :arg hash: + :class:`!scram` hash stored for desired user + + :type alg: str + :arg alg: + Name of digest algorithm (e.g. ``"sha-1"``) requested by client. + + This value is run through :func:`~passlib.crypto.digest.norm_hash_name`, + so it is case-insensitive, and can be the raw SCRAM + mechanism name (e.g. ``"SCRAM-SHA-1"``), the IANA name, + or the hashlib name. + + :raises KeyError: + If the hash does not contain an entry for the requested digest + algorithm. + + :returns: + A tuple containing ``(salt, rounds, digest)``, + where *digest* matches the raw bytes returned by + SCRAM's :func:`Hi` function for the stored password, + the provided *salt*, and the iteration count (*rounds*). + *salt* and *digest* are both raw (unencoded) bytes. + """ + # XXX: this could be sped up by writing custom parsing routine + # that just picks out relevant digest, and doesn't bother + # with full structure validation each time it's called. + alg = norm_hash_name(alg, 'iana') + self = cls.from_string(hash) + chkmap = self.checksum + if not chkmap: + raise ValueError("scram hash contains no digests") + return self.salt, self.rounds, chkmap[alg] + + @classmethod + def extract_digest_algs(cls, hash, format="iana"): + """Return names of all algorithms stored in a given hash. + + :type hash: str + :arg hash: + The :class:`!scram` hash to parse + + :type format: str + :param format: + This changes the naming convention used by the + returned algorithm names. By default the names + are IANA-compatible; possible values are ``"iana"`` or ``"hashlib"``. + + :returns: + Returns a list of digest algorithms; e.g. ``["sha-1"]`` + """ + # XXX: this could be sped up by writing custom parsing routine + # that just picks out relevant names, and doesn't bother + # with full structure validation each time it's called. + algs = cls.from_string(hash).algs + if format == "iana": + return algs + else: + return [norm_hash_name(alg, format) for alg in algs] + + @classmethod + def derive_digest(cls, password, salt, rounds, alg): + """helper to create SaltedPassword digest for SCRAM. + + This performs the step in the SCRAM protocol described as:: + + SaltedPassword := Hi(Normalize(password), salt, i) + + :type password: unicode or utf-8 bytes + :arg password: password to run through digest + + :type salt: bytes + :arg salt: raw salt data + + :type rounds: int + :arg rounds: number of iterations. + + :type alg: str + :arg alg: name of digest to use (e.g. ``"sha-1"``). + + :returns: + raw bytes of ``SaltedPassword`` + """ + if isinstance(password, bytes): + password = password.decode("utf-8") + # NOTE: pbkdf2_hmac() will encode secret & salt using utf-8, + # and handle normalizing alg name. + return pbkdf2_hmac(alg, saslprep(password), salt, rounds) + + #=================================================================== + # serialization + #=================================================================== + + @classmethod + def from_string(cls, hash): + hash = to_native_str(hash, "ascii", "hash") + if not hash.startswith("$scram$"): + raise uh.exc.InvalidHashError(cls) + parts = hash[7:].split("$") + if len(parts) != 3: + raise uh.exc.MalformedHashError(cls) + rounds_str, salt_str, chk_str = parts + + # decode rounds + rounds = int(rounds_str) + if rounds_str != str(rounds): # forbid zero padding, etc. + raise uh.exc.MalformedHashError(cls) + + # decode salt + try: + salt = ab64_decode(salt_str.encode("ascii")) + except TypeError: + raise uh.exc.MalformedHashError(cls) + + # decode algs/digest list + if not chk_str: + # scram hashes MUST have something here. + raise uh.exc.MalformedHashError(cls) + elif "=" in chk_str: + # comma-separated list of 'alg=digest' pairs + algs = None + chkmap = {} + for pair in chk_str.split(","): + alg, digest = pair.split("=") + try: + chkmap[alg] = ab64_decode(digest.encode("ascii")) + except TypeError: + raise uh.exc.MalformedHashError(cls) + else: + # comma-separated list of alg names, no digests + algs = chk_str + chkmap = None + + # return new object + return cls( + rounds=rounds, + salt=salt, + checksum=chkmap, + algs=algs, + ) + + def to_string(self): + salt = bascii_to_str(ab64_encode(self.salt)) + chkmap = self.checksum + chk_str = ",".join( + "%s=%s" % (alg, bascii_to_str(ab64_encode(chkmap[alg]))) + for alg in self.algs + ) + return '$scram$%d$%s$%s' % (self.rounds, salt, chk_str) + + #=================================================================== + # variant constructor + #=================================================================== + @classmethod + def using(cls, default_algs=None, algs=None, **kwds): + # parse aliases + if algs is not None: + assert default_algs is None + default_algs = algs + + # create subclass + subcls = super(scram, cls).using(**kwds) + + # fill in algs + if default_algs is not None: + subcls.default_algs = cls._norm_algs(default_algs) + return subcls + + #=================================================================== + # init + #=================================================================== + def __init__(self, algs=None, **kwds): + super(scram, self).__init__(**kwds) + + # init algs + digest_map = self.checksum + if algs is not None: + if digest_map is not None: + raise RuntimeError("checksum & algs kwds are mutually exclusive") + algs = self._norm_algs(algs) + elif digest_map is not None: + # derive algs list from digest map (if present). + algs = self._norm_algs(digest_map.keys()) + elif self.use_defaults: + algs = list(self.default_algs) + assert self._norm_algs(algs) == algs, "invalid default algs: %r" % (algs,) + else: + raise TypeError("no algs list specified") + self.algs = algs + + def _norm_checksum(self, checksum, relaxed=False): + if not isinstance(checksum, dict): + raise uh.exc.ExpectedTypeError(checksum, "dict", "checksum") + for alg, digest in iteritems(checksum): + if alg != norm_hash_name(alg, 'iana'): + raise ValueError("malformed algorithm name in scram hash: %r" % + (alg,)) + if len(alg) > 9: + raise ValueError("SCRAM limits algorithm names to " + "9 characters: %r" % (alg,)) + if not isinstance(digest, bytes): + raise uh.exc.ExpectedTypeError(digest, "raw bytes", "digests") + # TODO: verify digest size (if digest is known) + if 'sha-1' not in checksum: + # NOTE: required because of SCRAM spec. + raise ValueError("sha-1 must be in algorithm list of scram hash") + return checksum + + @classmethod + def _norm_algs(cls, algs): + """normalize algs parameter""" + if isinstance(algs, native_string_types): + algs = splitcomma(algs) + algs = sorted(norm_hash_name(alg, 'iana') for alg in algs) + if any(len(alg)>9 for alg in algs): + raise ValueError("SCRAM limits alg names to max of 9 characters") + if 'sha-1' not in algs: + # NOTE: required because of SCRAM spec (rfc 5802) + raise ValueError("sha-1 must be in algorithm list of scram hash") + return algs + + #=================================================================== + # migration + #=================================================================== + def _calc_needs_update(self, **kwds): + # marks hashes as deprecated if they don't include at least all default_algs. + # XXX: should we deprecate if they aren't exactly the same, + # to permit removing legacy hashes? + if not set(self.algs).issuperset(self.default_algs): + return True + + # hand off to base implementation + return super(scram, self)._calc_needs_update(**kwds) + + #=================================================================== + # digest methods + #=================================================================== + def _calc_checksum(self, secret, alg=None): + rounds = self.rounds + salt = self.salt + hash = self.derive_digest + if alg: + # if requested, generate digest for specific alg + return hash(secret, salt, rounds, alg) + else: + # by default, return dict containing digests for all algs + return dict( + (alg, hash(secret, salt, rounds, alg)) + for alg in self.algs + ) + + @classmethod + def verify(cls, secret, hash, full=False): + uh.validate_secret(secret) + self = cls.from_string(hash) + chkmap = self.checksum + if not chkmap: + raise ValueError("expected %s hash, got %s config string instead" % + (cls.name, cls.name)) + + # NOTE: to make the verify method efficient, we just calculate hash + # of shortest digest by default. apps can pass in "full=True" to + # check entire hash for consistency. + if full: + correct = failed = False + for alg, digest in iteritems(chkmap): + other = self._calc_checksum(secret, alg) + # NOTE: could do this length check in norm_algs(), + # but don't need to be that strict, and want to be able + # to parse hashes containing algs not supported by platform. + # it's fine if we fail here though. + if len(digest) != len(other): + raise ValueError("mis-sized %s digest in scram hash: %r != %r" + % (alg, len(digest), len(other))) + if consteq(other, digest): + correct = True + else: + failed = True + if correct and failed: + raise ValueError("scram hash verified inconsistently, " + "may be corrupted") + else: + return correct + else: + # XXX: should this just always use sha1 hash? would be faster. + # otherwise only verify against one hash, pick one w/ best security. + for alg in self._verify_algs: + if alg in chkmap: + other = self._calc_checksum(secret, alg) + return consteq(other, chkmap[alg]) + # there should always be sha-1 at the very least, + # or something went wrong inside _norm_algs() + raise AssertionError("sha-1 digest not found!") + + #=================================================================== + # + #=================================================================== + +#============================================================================= +# code used for testing scram against protocol examples during development. +#============================================================================= +##def _test_reference_scram(): +## "quick hack testing scram reference vectors" +## # NOTE: "n,," is GS2 header - see https://tools.ietf.org/html/rfc5801 +## from passlib.utils.compat import print_ +## +## engine = _scram_engine( +## alg="sha-1", +## salt='QSXCR+Q6sek8bf92'.decode("base64"), +## rounds=4096, +## password=u("pencil"), +## ) +## print_(engine.digest.encode("base64").rstrip()) +## +## msg = engine.format_auth_msg( +## username="user", +## client_nonce = "fyko+d2lbbFgONRv9qkxdawL", +## server_nonce = "3rfcNHYJY1ZVvWVs7j", +## header='c=biws', +## ) +## +## cp = engine.get_encoded_client_proof(msg) +## assert cp == "v0X8v3Bz2T0CJGbJQyF0X+HI4Ts=", cp +## +## ss = engine.get_encoded_server_sig(msg) +## assert ss == "rmF9pqV8S7suAoZWja4dJRkFsKQ=", ss +## +##class _scram_engine(object): +## """helper class for verifying scram hash behavior +## against SCRAM protocol examples. not officially part of Passlib. +## +## takes in alg, salt, rounds, and a digest or password. +## +## can calculate the various keys & messages of the scram protocol. +## +## """ +## #========================================================= +## # init +## #========================================================= +## +## @classmethod +## def from_string(cls, hash, alg): +## "create record from scram hash, for given alg" +## return cls(alg, *scram.extract_digest_info(hash, alg)) +## +## def __init__(self, alg, salt, rounds, digest=None, password=None): +## self.alg = norm_hash_name(alg) +## self.salt = salt +## self.rounds = rounds +## self.password = password +## if password: +## data = scram.derive_digest(password, salt, rounds, alg) +## if digest and data != digest: +## raise ValueError("password doesn't match digest") +## else: +## digest = data +## elif not digest: +## raise TypeError("must provide password or digest") +## self.digest = digest +## +## #========================================================= +## # frontend methods +## #========================================================= +## def get_hash(self, data): +## "return hash of raw data" +## return hashlib.new(iana_to_hashlib(self.alg), data).digest() +## +## def get_client_proof(self, msg): +## "return client proof of specified auth msg text" +## return xor_bytes(self.client_key, self.get_client_sig(msg)) +## +## def get_encoded_client_proof(self, msg): +## return self.get_client_proof(msg).encode("base64").rstrip() +## +## def get_client_sig(self, msg): +## "return client signature of specified auth msg text" +## return self.get_hmac(self.stored_key, msg) +## +## def get_server_sig(self, msg): +## "return server signature of specified auth msg text" +## return self.get_hmac(self.server_key, msg) +## +## def get_encoded_server_sig(self, msg): +## return self.get_server_sig(msg).encode("base64").rstrip() +## +## def format_server_response(self, client_nonce, server_nonce): +## return 'r={client_nonce}{server_nonce},s={salt},i={rounds}'.format( +## client_nonce=client_nonce, +## server_nonce=server_nonce, +## rounds=self.rounds, +## salt=self.encoded_salt, +## ) +## +## def format_auth_msg(self, username, client_nonce, server_nonce, +## header='c=biws'): +## return ( +## 'n={username},r={client_nonce}' +## ',' +## 'r={client_nonce}{server_nonce},s={salt},i={rounds}' +## ',' +## '{header},r={client_nonce}{server_nonce}' +## ).format( +## username=username, +## client_nonce=client_nonce, +## server_nonce=server_nonce, +## salt=self.encoded_salt, +## rounds=self.rounds, +## header=header, +## ) +## +## #========================================================= +## # helpers to calculate & cache constant data +## #========================================================= +## def _calc_get_hmac(self): +## return get_prf("hmac-" + iana_to_hashlib(self.alg))[0] +## +## def _calc_client_key(self): +## return self.get_hmac(self.digest, b("Client Key")) +## +## def _calc_stored_key(self): +## return self.get_hash(self.client_key) +## +## def _calc_server_key(self): +## return self.get_hmac(self.digest, b("Server Key")) +## +## def _calc_encoded_salt(self): +## return self.salt.encode("base64").rstrip() +## +## #========================================================= +## # hacks for calculated attributes +## #========================================================= +## +## def __getattr__(self, attr): +## if not attr.startswith("_"): +## f = getattr(self, "_calc_" + attr, None) +## if f: +## value = f() +## setattr(self, attr, value) +## return value +## raise AttributeError("attribute not found") +## +## def __dir__(self): +## cdir = dir(self.__class__) +## attrs = set(cdir) +## attrs.update(self.__dict__) +## attrs.update(attr[6:] for attr in cdir +## if attr.startswith("_calc_")) +## return sorted(attrs) +## #========================================================= +## # eoc +## #========================================================= + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/handlers/scrypt.py b/ansible/lib/python3.11/site-packages/passlib/handlers/scrypt.py new file mode 100644 index 000000000..1686fda50 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/handlers/scrypt.py @@ -0,0 +1,383 @@ +"""passlib.handlers.scrypt -- scrypt password hash""" +#============================================================================= +# imports +#============================================================================= +from __future__ import with_statement, absolute_import +# core +import logging; log = logging.getLogger(__name__) +# site +# pkg +from passlib.crypto import scrypt as _scrypt +from passlib.utils import h64, to_bytes +from passlib.utils.binary import h64, b64s_decode, b64s_encode +from passlib.utils.compat import u, bascii_to_str, suppress_cause +from passlib.utils.decor import classproperty +import passlib.utils.handlers as uh +# local +__all__ = [ + "scrypt", +] + +#============================================================================= +# scrypt format identifiers +#============================================================================= + +IDENT_SCRYPT = u("$scrypt$") # identifier used by passlib +IDENT_7 = u("$7$") # used by official scrypt spec + +_UDOLLAR = u("$") + +#============================================================================= +# handler +#============================================================================= +class scrypt(uh.ParallelismMixin, uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.HasManyIdents, + uh.GenericHandler): + """This class implements an SCrypt-based password [#scrypt-home]_ hash, and follows the :ref:`password-hash-api`. + + It supports a variable-length salt, a variable number of rounds, + as well as some custom tuning parameters unique to scrypt (see below). + + The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: + + :type salt: str + :param salt: + Optional salt string. + If specified, the length must be between 0-1024 bytes. + If not specified, one will be auto-generated (this is recommended). + + :type salt_size: int + :param salt_size: + Optional number of bytes to use when autogenerating new salts. + Defaults to 16 bytes, but can be any value between 0 and 1024. + + :type rounds: int + :param rounds: + Optional number of rounds to use. + Defaults to 16, but must be within ``range(1,32)``. + + .. warning:: + + Unlike many hash algorithms, increasing the rounds value + will increase both the time *and memory* required to hash a password. + + :type block_size: int + :param block_size: + Optional block size to pass to scrypt hash function (the ``r`` parameter). + Useful for tuning scrypt to optimal performance for your CPU architecture. + Defaults to 8. + + :type parallelism: int + :param parallelism: + Optional parallelism to pass to scrypt hash function (the ``p`` parameter). + Defaults to 1. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include ``rounds`` + that are too small or too large, and ``salt`` strings that are too long. + + .. note:: + + The underlying scrypt hash function has a number of limitations + on it's parameter values, which forbids certain combinations of settings. + The requirements are: + + * ``linear_rounds = 2**`` + * ``linear_rounds < 2**(16 * block_size)`` + * ``block_size * parallelism <= 2**30-1`` + + .. todo:: + + This class currently does not support configuring default values + for ``block_size`` or ``parallelism`` via a :class:`~passlib.context.CryptContext` + configuration. + """ + + #=================================================================== + # class attrs + #=================================================================== + + #------------------------ + # PasswordHash + #------------------------ + name = "scrypt" + setting_kwds = ("ident", "salt", "salt_size", "rounds", "block_size", "parallelism") + + #------------------------ + # GenericHandler + #------------------------ + # NOTE: scrypt supports arbitrary output sizes. since it's output runs through + # pbkdf2-hmac-sha256 before returning, and this could be raised eventually... + # but a 256-bit digest is more than sufficient for password hashing. + # XXX: make checksum size configurable? could merge w/ argon2 code that does this. + checksum_size = 32 + + #------------------------ + # HasManyIdents + #------------------------ + default_ident = IDENT_SCRYPT + ident_values = (IDENT_SCRYPT, IDENT_7) + + #------------------------ + # HasRawSalt + #------------------------ + default_salt_size = 16 + max_salt_size = 1024 + + #------------------------ + # HasRounds + #------------------------ + # TODO: would like to dynamically pick this based on system + default_rounds = 16 + min_rounds = 1 + max_rounds = 31 # limited by scrypt alg + rounds_cost = "log2" + + # TODO: make default block size configurable via using(), and deprecatable via .needs_update() + + #=================================================================== + # instance attrs + #=================================================================== + + #: default parallelism setting (min=1 currently hardcoded in mixin) + parallelism = 1 + + #: default block size setting + block_size = 8 + + #=================================================================== + # variant constructor + #=================================================================== + + @classmethod + def using(cls, block_size=None, **kwds): + subcls = super(scrypt, cls).using(**kwds) + if block_size is not None: + if isinstance(block_size, uh.native_string_types): + block_size = int(block_size) + subcls.block_size = subcls._norm_block_size(block_size, relaxed=kwds.get("relaxed")) + + # make sure param combination is valid for scrypt() + try: + _scrypt.validate(1 << cls.default_rounds, cls.block_size, cls.parallelism) + except ValueError as err: + raise suppress_cause(ValueError("scrypt: invalid settings combination: " + str(err))) + + return subcls + + #=================================================================== + # parsing + #=================================================================== + + @classmethod + def from_string(cls, hash): + return cls(**cls.parse(hash)) + + @classmethod + def parse(cls, hash): + ident, suffix = cls._parse_ident(hash) + func = getattr(cls, "_parse_%s_string" % ident.strip(_UDOLLAR), None) + if func: + return func(suffix) + else: + raise uh.exc.InvalidHashError(cls) + + # + # passlib's format: + # $scrypt$ln=,r=,p=

$[$] + # where: + # logN, r, p -- decimal-encoded positive integer, no zero-padding + # logN -- log cost setting + # r -- block size setting (usually 8) + # p -- parallelism setting (usually 1) + # salt, digest -- b64-nopad encoded bytes + # + + @classmethod + def _parse_scrypt_string(cls, suffix): + # break params, salt, and digest sections + parts = suffix.split("$") + if len(parts) == 3: + params, salt, digest = parts + elif len(parts) == 2: + params, salt = parts + digest = None + else: + raise uh.exc.MalformedHashError(cls, "malformed hash") + + # break params apart + parts = params.split(",") + if len(parts) == 3: + nstr, bstr, pstr = parts + assert nstr.startswith("ln=") + assert bstr.startswith("r=") + assert pstr.startswith("p=") + else: + raise uh.exc.MalformedHashError(cls, "malformed settings field") + + return dict( + ident=IDENT_SCRYPT, + rounds=int(nstr[3:]), + block_size=int(bstr[2:]), + parallelism=int(pstr[2:]), + salt=b64s_decode(salt.encode("ascii")), + checksum=b64s_decode(digest.encode("ascii")) if digest else None, + ) + + # + # official format specification defined at + # https://gitlab.com/jas/scrypt-unix-crypt/blob/master/unix-scrypt.txt + # format: + # $7$[$] + # 0 12345 67890 1 + # where: + # All bytes use h64-little-endian encoding + # N: 6-bit log cost setting + # r: 30-bit block size setting + # p: 30-bit parallelism setting + # salt: variable length salt bytes + # digest: fixed 32-byte digest + # + + @classmethod + def _parse_7_string(cls, suffix): + # XXX: annoyingly, official spec embeds salt *raw*, yet doesn't specify a hash encoding. + # so assuming only h64 chars are valid for salt, and are ASCII encoded. + + # split into params & digest + parts = suffix.encode("ascii").split(b"$") + if len(parts) == 2: + params, digest = parts + elif len(parts) == 1: + params, = parts + digest = None + else: + raise uh.exc.MalformedHashError() + + # parse params & return + if len(params) < 11: + raise uh.exc.MalformedHashError(cls, "params field too short") + return dict( + ident=IDENT_7, + rounds=h64.decode_int6(params[:1]), + block_size=h64.decode_int30(params[1:6]), + parallelism=h64.decode_int30(params[6:11]), + salt=params[11:], + checksum=h64.decode_bytes(digest) if digest else None, + ) + + #=================================================================== + # formatting + #=================================================================== + def to_string(self): + ident = self.ident + if ident == IDENT_SCRYPT: + return "$scrypt$ln=%d,r=%d,p=%d$%s$%s" % ( + self.rounds, + self.block_size, + self.parallelism, + bascii_to_str(b64s_encode(self.salt)), + bascii_to_str(b64s_encode(self.checksum)), + ) + else: + assert ident == IDENT_7 + salt = self.salt + try: + salt.decode("ascii") + except UnicodeDecodeError: + raise suppress_cause(NotImplementedError("scrypt $7$ hashes dont support non-ascii salts")) + return bascii_to_str(b"".join([ + b"$7$", + h64.encode_int6(self.rounds), + h64.encode_int30(self.block_size), + h64.encode_int30(self.parallelism), + self.salt, + b"$", + h64.encode_bytes(self.checksum) + ])) + + #=================================================================== + # init + #=================================================================== + def __init__(self, block_size=None, **kwds): + super(scrypt, self).__init__(**kwds) + + # init block size + if block_size is None: + assert uh.validate_default_value(self, self.block_size, self._norm_block_size, + param="block_size") + else: + self.block_size = self._norm_block_size(block_size) + + # NOTE: if hash contains invalid complex constraint, relying on error + # being raised by scrypt call in _calc_checksum() + + @classmethod + def _norm_block_size(cls, block_size, relaxed=False): + return uh.norm_integer(cls, block_size, min=1, param="block_size", relaxed=relaxed) + + def _generate_salt(self): + salt = super(scrypt, self)._generate_salt() + if self.ident == IDENT_7: + # this format doesn't support non-ascii salts. + # as workaround, we take raw bytes, encoded to base64 + salt = b64s_encode(salt) + return salt + + #=================================================================== + # backend configuration + # NOTE: this following HasManyBackends' API, but provides it's own implementation, + # which actually switches the backend that 'passlib.crypto.scrypt.scrypt()' uses. + #=================================================================== + + @classproperty + def backends(cls): + return _scrypt.backend_values + + @classmethod + def get_backend(cls): + return _scrypt.backend + + @classmethod + def has_backend(cls, name="any"): + try: + cls.set_backend(name, dryrun=True) + return True + except uh.exc.MissingBackendError: + return False + + @classmethod + def set_backend(cls, name="any", dryrun=False): + _scrypt._set_backend(name, dryrun=dryrun) + + #=================================================================== + # digest calculation + #=================================================================== + def _calc_checksum(self, secret): + secret = to_bytes(secret, param="secret") + return _scrypt.scrypt(secret, self.salt, n=(1 << self.rounds), r=self.block_size, + p=self.parallelism, keylen=self.checksum_size) + + #=================================================================== + # hash migration + #=================================================================== + + def _calc_needs_update(self, **kwds): + """ + mark hash as needing update if rounds is outside desired bounds. + """ + # XXX: for now, marking all hashes which don't have matching block_size setting + if self.block_size != type(self).block_size: + return True + return super(scrypt, self)._calc_needs_update(**kwds) + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/handlers/sha1_crypt.py b/ansible/lib/python3.11/site-packages/passlib/handlers/sha1_crypt.py new file mode 100644 index 000000000..8f9aa7173 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/handlers/sha1_crypt.py @@ -0,0 +1,158 @@ +"""passlib.handlers.sha1_crypt +""" + +#============================================================================= +# imports +#============================================================================= + +# core +import logging; log = logging.getLogger(__name__) +# site +# pkg +from passlib.utils import safe_crypt, test_crypt +from passlib.utils.binary import h64 +from passlib.utils.compat import u, unicode, irange +from passlib.crypto.digest import compile_hmac +import passlib.utils.handlers as uh +# local +__all__ = [ +] +#============================================================================= +# sha1-crypt +#============================================================================= +_BNULL = b'\x00' + +class sha1_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler): + """This class implements the SHA1-Crypt password hash, and follows the :ref:`password-hash-api`. + + It supports a variable-length salt, and a variable number of rounds. + + The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: + + :type salt: str + :param salt: + Optional salt string. + If not specified, an 8 character one will be autogenerated (this is recommended). + If specified, it must be 0-64 characters, drawn from the regexp range ``[./0-9A-Za-z]``. + + :type salt_size: int + :param salt_size: + Optional number of bytes to use when autogenerating new salts. + Defaults to 8 bytes, but can be any value between 0 and 64. + + :type rounds: int + :param rounds: + Optional number of rounds to use. + Defaults to 480000, must be between 1 and 4294967295, inclusive. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include ``rounds`` + that are too small or too large, and ``salt`` strings that are too long. + + .. versionadded:: 1.6 + """ + + #=================================================================== + # class attrs + #=================================================================== + #--GenericHandler-- + name = "sha1_crypt" + setting_kwds = ("salt", "salt_size", "rounds") + ident = u("$sha1$") + checksum_size = 28 + checksum_chars = uh.HASH64_CHARS + + #--HasSalt-- + default_salt_size = 8 + max_salt_size = 64 + salt_chars = uh.HASH64_CHARS + + #--HasRounds-- + default_rounds = 480000 # current passlib default + min_rounds = 1 # really, this should be higher. + max_rounds = 4294967295 # 32-bit integer limit + rounds_cost = "linear" + + #=================================================================== + # formatting + #=================================================================== + @classmethod + def from_string(cls, hash): + rounds, salt, chk = uh.parse_mc3(hash, cls.ident, handler=cls) + return cls(rounds=rounds, salt=salt, checksum=chk) + + def to_string(self, config=False): + chk = None if config else self.checksum + return uh.render_mc3(self.ident, self.rounds, self.salt, chk) + + #=================================================================== + # backend + #=================================================================== + backends = ("os_crypt", "builtin") + + #--------------------------------------------------------------- + # os_crypt backend + #--------------------------------------------------------------- + @classmethod + def _load_backend_os_crypt(cls): + if test_crypt("test", '$sha1$1$Wq3GL2Vp$C8U25GvfHS8qGHim' + 'ExLaiSFlGkAe'): + cls._set_calc_checksum_backend(cls._calc_checksum_os_crypt) + return True + else: + return False + + def _calc_checksum_os_crypt(self, secret): + config = self.to_string(config=True) + hash = safe_crypt(secret, config) + if hash is None: + # py3's crypt.crypt() can't handle non-utf8 bytes. + # fallback to builtin alg, which is always available. + return self._calc_checksum_builtin(secret) + if not hash.startswith(config) or len(hash) != len(config) + 29: + raise uh.exc.CryptBackendError(self, config, hash) + return hash[-28:] + + #--------------------------------------------------------------- + # builtin backend + #--------------------------------------------------------------- + @classmethod + def _load_backend_builtin(cls): + cls._set_calc_checksum_backend(cls._calc_checksum_builtin) + return True + + def _calc_checksum_builtin(self, secret): + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + if _BNULL in secret: + raise uh.exc.NullPasswordError(self) + rounds = self.rounds + # NOTE: this seed value is NOT the same as the config string + result = (u("%s$sha1$%s") % (self.salt, rounds)).encode("ascii") + # NOTE: this algorithm is essentially PBKDF1, modified to use HMAC. + keyed_hmac = compile_hmac("sha1", secret) + for _ in irange(rounds): + result = keyed_hmac(result) + return h64.encode_transposed_bytes(result, self._chk_offsets).decode("ascii") + + _chk_offsets = [ + 2,1,0, + 5,4,3, + 8,7,6, + 11,10,9, + 14,13,12, + 17,16,15, + 0,19,18, + ] + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/handlers/sha2_crypt.py b/ansible/lib/python3.11/site-packages/passlib/handlers/sha2_crypt.py new file mode 100644 index 000000000..e6060c5e9 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/handlers/sha2_crypt.py @@ -0,0 +1,534 @@ +"""passlib.handlers.sha2_crypt - SHA256-Crypt / SHA512-Crypt""" +#============================================================================= +# imports +#============================================================================= +# core +import hashlib +import logging; log = logging.getLogger(__name__) +# site +# pkg +from passlib.utils import safe_crypt, test_crypt, \ + repeat_string, to_unicode +from passlib.utils.binary import h64 +from passlib.utils.compat import byte_elem_value, u, \ + uascii_to_str, unicode +import passlib.utils.handlers as uh +# local +__all__ = [ + "sha512_crypt", + "sha256_crypt", +] + +#============================================================================= +# pure-python backend, used by both sha256_crypt & sha512_crypt +# when crypt.crypt() backend is not available. +#============================================================================= +_BNULL = b'\x00' + +# pre-calculated offsets used to speed up C digest stage (see notes below). +# sequence generated using the following: + ##perms_order = "p,pp,ps,psp,sp,spp".split(",") + ##def offset(i): + ## key = (("p" if i % 2 else "") + ("s" if i % 3 else "") + + ## ("p" if i % 7 else "") + ("" if i % 2 else "p")) + ## return perms_order.index(key) + ##_c_digest_offsets = [(offset(i), offset(i+1)) for i in range(0,42,2)] +_c_digest_offsets = ( + (0, 3), (5, 1), (5, 3), (1, 2), (5, 1), (5, 3), (1, 3), + (4, 1), (5, 3), (1, 3), (5, 0), (5, 3), (1, 3), (5, 1), + (4, 3), (1, 3), (5, 1), (5, 2), (1, 3), (5, 1), (5, 3), + ) + +# map used to transpose bytes when encoding final sha256_crypt digest +_256_transpose_map = ( + 20, 10, 0, 11, 1, 21, 2, 22, 12, 23, 13, 3, 14, 4, 24, 5, + 25, 15, 26, 16, 6, 17, 7, 27, 8, 28, 18, 29, 19, 9, 30, 31, +) + +# map used to transpose bytes when encoding final sha512_crypt digest +_512_transpose_map = ( + 42, 21, 0, 1, 43, 22, 23, 2, 44, 45, 24, 3, 4, 46, 25, 26, + 5, 47, 48, 27, 6, 7, 49, 28, 29, 8, 50, 51, 30, 9, 10, 52, + 31, 32, 11, 53, 54, 33, 12, 13, 55, 34, 35, 14, 56, 57, 36, 15, + 16, 58, 37, 38, 17, 59, 60, 39, 18, 19, 61, 40, 41, 20, 62, 63, +) + +def _raw_sha2_crypt(pwd, salt, rounds, use_512=False): + """perform raw sha256-crypt / sha512-crypt + + this function provides a pure-python implementation of the internals + for the SHA256-Crypt and SHA512-Crypt algorithms; it doesn't + handle any of the parsing/validation of the hash strings themselves. + + :arg pwd: password chars/bytes to hash + :arg salt: salt chars to use + :arg rounds: linear rounds cost + :arg use_512: use sha512-crypt instead of sha256-crypt mode + + :returns: + encoded checksum chars + """ + #=================================================================== + # init & validate inputs + #=================================================================== + + # NOTE: the setup portion of this algorithm scales ~linearly in time + # with the size of the password, making it vulnerable to a DOS from + # unreasonably large inputs. the following code has some optimizations + # which would make things even worse, using O(pwd_len**2) memory + # when calculating digest P. + # + # to mitigate these two issues: 1) this code switches to a + # O(pwd_len)-memory algorithm for passwords that are much larger + # than average, and 2) Passlib enforces a library-wide max limit on + # the size of passwords it will allow, to prevent this algorithm and + # others from being DOSed in this way (see passlib.exc.PasswordSizeError + # for details). + + # validate secret + if isinstance(pwd, unicode): + # XXX: not sure what official unicode policy is, using this as default + pwd = pwd.encode("utf-8") + assert isinstance(pwd, bytes) + if _BNULL in pwd: + raise uh.exc.NullPasswordError(sha512_crypt if use_512 else sha256_crypt) + pwd_len = len(pwd) + + # validate rounds + assert 1000 <= rounds <= 999999999, "invalid rounds" + # NOTE: spec says out-of-range rounds should be clipped, instead of + # causing an error. this function assumes that's been taken care of + # by the handler class. + + # validate salt + assert isinstance(salt, unicode), "salt not unicode" + salt = salt.encode("ascii") + salt_len = len(salt) + assert salt_len < 17, "salt too large" + # NOTE: spec says salts larger than 16 bytes should be truncated, + # instead of causing an error. this function assumes that's been + # taken care of by the handler class. + + # load sha256/512 specific constants + if use_512: + hash_const = hashlib.sha512 + transpose_map = _512_transpose_map + else: + hash_const = hashlib.sha256 + transpose_map = _256_transpose_map + + #=================================================================== + # digest B - used as subinput to digest A + #=================================================================== + db = hash_const(pwd + salt + pwd).digest() + + #=================================================================== + # digest A - used to initialize first round of digest C + #=================================================================== + # start out with pwd + salt + a_ctx = hash_const(pwd + salt) + a_ctx_update = a_ctx.update + + # add pwd_len bytes of b, repeating b as many times as needed. + a_ctx_update(repeat_string(db, pwd_len)) + + # for each bit in pwd_len: add b if it's 1, or pwd if it's 0 + i = pwd_len + while i: + a_ctx_update(db if i & 1 else pwd) + i >>= 1 + + # finish A + da = a_ctx.digest() + + #=================================================================== + # digest P from password - used instead of password itself + # when calculating digest C. + #=================================================================== + if pwd_len < 96: + # this method is faster under python, but uses O(pwd_len**2) memory; + # so we don't use it for larger passwords to avoid a potential DOS. + dp = repeat_string(hash_const(pwd * pwd_len).digest(), pwd_len) + else: + # this method is slower under python, but uses a fixed amount of memory. + tmp_ctx = hash_const(pwd) + tmp_ctx_update = tmp_ctx.update + i = pwd_len-1 + while i: + tmp_ctx_update(pwd) + i -= 1 + dp = repeat_string(tmp_ctx.digest(), pwd_len) + assert len(dp) == pwd_len + + #=================================================================== + # digest S - used instead of salt itself when calculating digest C + #=================================================================== + ds = hash_const(salt * (16 + byte_elem_value(da[0]))).digest()[:salt_len] + assert len(ds) == salt_len, "salt_len somehow > hash_len!" + + #=================================================================== + # digest C - for a variable number of rounds, combine A, S, and P + # digests in various ways; in order to burn CPU time. + #=================================================================== + + # NOTE: the original SHA256/512-Crypt specification performs the C digest + # calculation using the following loop: + # + ##dc = da + ##i = 0 + ##while i < rounds: + ## tmp_ctx = hash_const(dp if i & 1 else dc) + ## if i % 3: + ## tmp_ctx.update(ds) + ## if i % 7: + ## tmp_ctx.update(dp) + ## tmp_ctx.update(dc if i & 1 else dp) + ## dc = tmp_ctx.digest() + ## i += 1 + # + # The code Passlib uses (below) implements an equivalent algorithm, + # it's just been heavily optimized to pre-calculate a large number + # of things beforehand. It works off of a couple of observations + # about the original algorithm: + # + # 1. each round is a combination of 'dc', 'ds', and 'dp'; determined + # by the whether 'i' a multiple of 2,3, and/or 7. + # 2. since lcm(2,3,7)==42, the series of combinations will repeat + # every 42 rounds. + # 3. even rounds 0-40 consist of 'hash(dc + round-specific-constant)'; + # while odd rounds 1-41 consist of hash(round-specific-constant + dc) + # + # Using these observations, the following code... + # * calculates the round-specific combination of ds & dp for each round 0-41 + # * runs through as many 42-round blocks as possible + # * runs through as many pairs of rounds as possible for remaining rounds + # * performs once last round if the total rounds should be odd. + # + # this cuts out a lot of the control overhead incurred when running the + # original loop 40,000+ times in python, resulting in ~20% increase in + # speed under CPython (though still 2x slower than glibc crypt) + + # prepare the 6 combinations of ds & dp which are needed + # (order of 'perms' must match how _c_digest_offsets was generated) + dp_dp = dp+dp + dp_ds = dp+ds + perms = [dp, dp_dp, dp_ds, dp_ds+dp, ds+dp, ds+dp_dp] + + # build up list of even-round & odd-round constants, + # and store in 21-element list as (even,odd) pairs. + data = [ (perms[even], perms[odd]) for even, odd in _c_digest_offsets] + + # perform as many full 42-round blocks as possible + dc = da + blocks, tail = divmod(rounds, 42) + while blocks: + for even, odd in data: + dc = hash_const(odd + hash_const(dc + even).digest()).digest() + blocks -= 1 + + # perform any leftover rounds + if tail: + # perform any pairs of rounds + pairs = tail>>1 + for even, odd in data[:pairs]: + dc = hash_const(odd + hash_const(dc + even).digest()).digest() + + # if rounds was odd, do one last round (since we started at 0, + # last round will be an even-numbered round) + if tail & 1: + dc = hash_const(dc + data[pairs][0]).digest() + + #=================================================================== + # encode digest using appropriate transpose map + #=================================================================== + return h64.encode_transposed_bytes(dc, transpose_map).decode("ascii") + +#============================================================================= +# handlers +#============================================================================= +_UROUNDS = u("rounds=") +_UDOLLAR = u("$") +_UZERO = u("0") + +class _SHA2_Common(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, + uh.GenericHandler): + """class containing common code shared by sha256_crypt & sha512_crypt""" + #=================================================================== + # class attrs + #=================================================================== + # name - set by subclass + setting_kwds = ("salt", "rounds", "implicit_rounds", "salt_size") + # ident - set by subclass + checksum_chars = uh.HASH64_CHARS + # checksum_size - set by subclass + + max_salt_size = 16 + salt_chars = uh.HASH64_CHARS + + min_rounds = 1000 # bounds set by spec + max_rounds = 999999999 # bounds set by spec + rounds_cost = "linear" + + _cdb_use_512 = False # flag for _calc_digest_builtin() + _rounds_prefix = None # ident + _UROUNDS + + #=================================================================== + # methods + #=================================================================== + implicit_rounds = False + + def __init__(self, implicit_rounds=None, **kwds): + super(_SHA2_Common, self).__init__(**kwds) + # if user calls hash() w/ 5000 rounds, default to compact form. + if implicit_rounds is None: + implicit_rounds = (self.use_defaults and self.rounds == 5000) + self.implicit_rounds = implicit_rounds + + def _parse_salt(self, salt): + # required per SHA2-crypt spec -- truncate config salts rather than throwing error + return self._norm_salt(salt, relaxed=self.checksum is None) + + def _parse_rounds(self, rounds): + # required per SHA2-crypt spec -- clip config rounds rather than throwing error + return self._norm_rounds(rounds, relaxed=self.checksum is None) + + @classmethod + def from_string(cls, hash): + # basic format this parses - + # $5$[rounds=$][$] + + # TODO: this *could* use uh.parse_mc3(), except that the rounds + # portion has a slightly different grammar. + + # convert to unicode, check for ident prefix, split on dollar signs. + hash = to_unicode(hash, "ascii", "hash") + ident = cls.ident + if not hash.startswith(ident): + raise uh.exc.InvalidHashError(cls) + assert len(ident) == 3 + parts = hash[3:].split(_UDOLLAR) + + # extract rounds value + if parts[0].startswith(_UROUNDS): + assert len(_UROUNDS) == 7 + rounds = parts.pop(0)[7:] + if rounds.startswith(_UZERO) and rounds != _UZERO: + raise uh.exc.ZeroPaddedRoundsError(cls) + rounds = int(rounds) + implicit_rounds = False + else: + rounds = 5000 + implicit_rounds = True + + # rest should be salt and checksum + if len(parts) == 2: + salt, chk = parts + elif len(parts) == 1: + salt = parts[0] + chk = None + else: + raise uh.exc.MalformedHashError(cls) + + # return new object + return cls( + rounds=rounds, + salt=salt, + checksum=chk or None, + implicit_rounds=implicit_rounds, + ) + + def to_string(self): + if self.rounds == 5000 and self.implicit_rounds: + hash = u("%s%s$%s") % (self.ident, self.salt, + self.checksum or u('')) + else: + hash = u("%srounds=%d$%s$%s") % (self.ident, self.rounds, + self.salt, self.checksum or u('')) + return uascii_to_str(hash) + + #=================================================================== + # backends + #=================================================================== + backends = ("os_crypt", "builtin") + + #--------------------------------------------------------------- + # os_crypt backend + #--------------------------------------------------------------- + + #: test hash for OS detection -- provided by subclass + _test_hash = None + + @classmethod + def _load_backend_os_crypt(cls): + if test_crypt(*cls._test_hash): + cls._set_calc_checksum_backend(cls._calc_checksum_os_crypt) + return True + else: + return False + + def _calc_checksum_os_crypt(self, secret): + config = self.to_string() + hash = safe_crypt(secret, config) + if hash is None: + # py3's crypt.crypt() can't handle non-utf8 bytes. + # fallback to builtin alg, which is always available. + return self._calc_checksum_builtin(secret) + # NOTE: avoiding full parsing routine via from_string().checksum, + # and just extracting the bit we need. + cs = self.checksum_size + if not hash.startswith(self.ident) or hash[-cs-1] != _UDOLLAR: + raise uh.exc.CryptBackendError(self, config, hash) + return hash[-cs:] + + #--------------------------------------------------------------- + # builtin backend + #--------------------------------------------------------------- + @classmethod + def _load_backend_builtin(cls): + cls._set_calc_checksum_backend(cls._calc_checksum_builtin) + return True + + def _calc_checksum_builtin(self, secret): + return _raw_sha2_crypt(secret, self.salt, self.rounds, + self._cdb_use_512) + + #=================================================================== + # eoc + #=================================================================== + +class sha256_crypt(_SHA2_Common): + """This class implements the SHA256-Crypt password hash, and follows the :ref:`password-hash-api`. + + It supports a variable-length salt, and a variable number of rounds. + + The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: + + :type salt: str + :param salt: + Optional salt string. + If not specified, one will be autogenerated (this is recommended). + If specified, it must be 0-16 characters, drawn from the regexp range ``[./0-9A-Za-z]``. + + :type rounds: int + :param rounds: + Optional number of rounds to use. + Defaults to 535000, must be between 1000 and 999999999, inclusive. + + .. note:: + per the official specification, when the rounds parameter is set to 5000, + it may be omitted from the hash string. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include ``rounds`` + that are too small or too large, and ``salt`` strings that are too long. + + .. versionadded:: 1.6 + + .. + commented out, currently only supported by :meth:`hash`, and not via :meth:`using`: + + :type implicit_rounds: bool + :param implicit_rounds: + this is an internal option which generally doesn't need to be touched. + + this flag determines whether the hash should omit the rounds parameter + when encoding it to a string; this is only permitted by the spec for rounds=5000, + and the flag is ignored otherwise. the spec requires the two different + encodings be preserved as they are, instead of normalizing them. + """ + #=================================================================== + # class attrs + #=================================================================== + name = "sha256_crypt" + ident = u("$5$") + checksum_size = 43 + # NOTE: using 25/75 weighting of builtin & os_crypt backends + default_rounds = 535000 + + #=================================================================== + # backends + #=================================================================== + _test_hash = ("test", "$5$rounds=1000$test$QmQADEXMG8POI5W" + "Dsaeho0P36yK3Tcrgboabng6bkb/") + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# sha 512 crypt +#============================================================================= +class sha512_crypt(_SHA2_Common): + """This class implements the SHA512-Crypt password hash, and follows the :ref:`password-hash-api`. + + It supports a variable-length salt, and a variable number of rounds. + + The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: + + :type salt: str + :param salt: + Optional salt string. + If not specified, one will be autogenerated (this is recommended). + If specified, it must be 0-16 characters, drawn from the regexp range ``[./0-9A-Za-z]``. + + :type rounds: int + :param rounds: + Optional number of rounds to use. + Defaults to 656000, must be between 1000 and 999999999, inclusive. + + .. note:: + per the official specification, when the rounds parameter is set to 5000, + it may be omitted from the hash string. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include ``rounds`` + that are too small or too large, and ``salt`` strings that are too long. + + .. versionadded:: 1.6 + + .. + commented out, currently only supported by :meth:`hash`, and not via :meth:`using`: + + :type implicit_rounds: bool + :param implicit_rounds: + this is an internal option which generally doesn't need to be touched. + + this flag determines whether the hash should omit the rounds parameter + when encoding it to a string; this is only permitted by the spec for rounds=5000, + and the flag is ignored otherwise. the spec requires the two different + encodings be preserved as they are, instead of normalizing them. + """ + + #=================================================================== + # class attrs + #=================================================================== + name = "sha512_crypt" + ident = u("$6$") + checksum_size = 86 + _cdb_use_512 = True + # NOTE: using 25/75 weighting of builtin & os_crypt backends + default_rounds = 656000 + + #=================================================================== + # backend + #=================================================================== + _test_hash = ("test", "$6$rounds=1000$test$2M/Lx6Mtobqj" + "Ljobw0Wmo4Q5OFx5nVLJvmgseatA6oMn" + "yWeBdRDx4DU.1H3eGmse6pgsOgDisWBG" + "I5c7TZauS0") + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/handlers/sun_md5_crypt.py b/ansible/lib/python3.11/site-packages/passlib/handlers/sun_md5_crypt.py new file mode 100644 index 000000000..0eeb4e744 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/handlers/sun_md5_crypt.py @@ -0,0 +1,363 @@ +"""passlib.handlers.sun_md5_crypt - Sun's Md5 Crypt, used on Solaris + +.. warning:: + + This implementation may not reproduce + the original Solaris behavior in some border cases. + See documentation for details. +""" + +#============================================================================= +# imports +#============================================================================= +# core +from hashlib import md5 +import re +import logging; log = logging.getLogger(__name__) +from warnings import warn +# site +# pkg +from passlib.utils import to_unicode +from passlib.utils.binary import h64 +from passlib.utils.compat import byte_elem_value, irange, u, \ + uascii_to_str, unicode, str_to_bascii +import passlib.utils.handlers as uh +# local +__all__ = [ + "sun_md5_crypt", +] + +#============================================================================= +# backend +#============================================================================= +# constant data used by alg - Hamlet act 3 scene 1 + null char +# exact bytes as in http://www.ibiblio.org/pub/docs/books/gutenberg/etext98/2ws2610.txt +# from Project Gutenberg. + +MAGIC_HAMLET = ( + b"To be, or not to be,--that is the question:--\n" + b"Whether 'tis nobler in the mind to suffer\n" + b"The slings and arrows of outrageous fortune\n" + b"Or to take arms against a sea of troubles,\n" + b"And by opposing end them?--To die,--to sleep,--\n" + b"No more; and by a sleep to say we end\n" + b"The heartache, and the thousand natural shocks\n" + b"That flesh is heir to,--'tis a consummation\n" + b"Devoutly to be wish'd. To die,--to sleep;--\n" + b"To sleep! perchance to dream:--ay, there's the rub;\n" + b"For in that sleep of death what dreams may come,\n" + b"When we have shuffled off this mortal coil,\n" + b"Must give us pause: there's the respect\n" + b"That makes calamity of so long life;\n" + b"For who would bear the whips and scorns of time,\n" + b"The oppressor's wrong, the proud man's contumely,\n" + b"The pangs of despis'd love, the law's delay,\n" + b"The insolence of office, and the spurns\n" + b"That patient merit of the unworthy takes,\n" + b"When he himself might his quietus make\n" + b"With a bare bodkin? who would these fardels bear,\n" + b"To grunt and sweat under a weary life,\n" + b"But that the dread of something after death,--\n" + b"The undiscover'd country, from whose bourn\n" + b"No traveller returns,--puzzles the will,\n" + b"And makes us rather bear those ills we have\n" + b"Than fly to others that we know not of?\n" + b"Thus conscience does make cowards of us all;\n" + b"And thus the native hue of resolution\n" + b"Is sicklied o'er with the pale cast of thought;\n" + b"And enterprises of great pith and moment,\n" + b"With this regard, their currents turn awry,\n" + b"And lose the name of action.--Soft you now!\n" + b"The fair Ophelia!--Nymph, in thy orisons\n" + b"Be all my sins remember'd.\n\x00" #<- apparently null at end of C string is included (test vector won't pass otherwise) +) + +# NOTE: these sequences are pre-calculated iteration ranges used by X & Y loops w/in rounds function below +xr = irange(7) +_XY_ROUNDS = [ + tuple((i,i,i+3) for i in xr), # xrounds 0 + tuple((i,i+1,i+4) for i in xr), # xrounds 1 + tuple((i,i+8,(i+11)&15) for i in xr), # yrounds 0 + tuple((i,(i+9)&15, (i+12)&15) for i in xr), # yrounds 1 +] +del xr + +def raw_sun_md5_crypt(secret, rounds, salt): + """given secret & salt, return encoded sun-md5-crypt checksum""" + global MAGIC_HAMLET + assert isinstance(secret, bytes) + assert isinstance(salt, bytes) + + # validate rounds + if rounds <= 0: + rounds = 0 + real_rounds = 4096 + rounds + # NOTE: spec seems to imply max 'rounds' is 2**32-1 + + # generate initial digest to start off round 0. + # NOTE: algorithm 'salt' includes full config string w/ trailing "$" + result = md5(secret + salt).digest() + assert len(result) == 16 + + # NOTE: many things in this function have been inlined (to speed up the loop + # as much as possible), to the point that this code barely resembles + # the algorithm as described in the docs. in particular: + # + # * all accesses to a given bit have been inlined using the formula + # rbitval(bit) = (rval((bit>>3) & 15) >> (bit & 7)) & 1 + # + # * the calculation of coinflip value R has been inlined + # + # * the conditional division of coinflip value V has been inlined as + # a shift right of 0 or 1. + # + # * the i, i+3, etc iterations are precalculated in lists. + # + # * the round-based conditional division of x & y is now performed + # by choosing an appropriate precalculated list, so that it only + # calculates the 7 bits which will actually be used. + # + X_ROUNDS_0, X_ROUNDS_1, Y_ROUNDS_0, Y_ROUNDS_1 = _XY_ROUNDS + + # NOTE: % appears to be *slightly* slower than &, so we prefer & if possible + + round = 0 + while round < real_rounds: + # convert last result byte string to list of byte-ints for easy access + rval = [ byte_elem_value(c) for c in result ].__getitem__ + + # build up X bit by bit + x = 0 + xrounds = X_ROUNDS_1 if (rval((round>>3) & 15)>>(round & 7)) & 1 else X_ROUNDS_0 + for i, ia, ib in xrounds: + a = rval(ia) + b = rval(ib) + v = rval((a >> (b % 5)) & 15) >> ((b>>(a&7)) & 1) + x |= ((rval((v>>3)&15)>>(v&7))&1) << i + + # build up Y bit by bit + y = 0 + yrounds = Y_ROUNDS_1 if (rval(((round+64)>>3) & 15)>>(round & 7)) & 1 else Y_ROUNDS_0 + for i, ia, ib in yrounds: + a = rval(ia) + b = rval(ib) + v = rval((a >> (b % 5)) & 15) >> ((b>>(a&7)) & 1) + y |= ((rval((v>>3)&15)>>(v&7))&1) << i + + # extract x'th and y'th bit, xoring them together to yeild "coin flip" + coin = ((rval(x>>3) >> (x&7)) ^ (rval(y>>3) >> (y&7))) & 1 + + # construct hash for this round + h = md5(result) + if coin: + h.update(MAGIC_HAMLET) + h.update(unicode(round).encode("ascii")) + result = h.digest() + + round += 1 + + # encode output + return h64.encode_transposed_bytes(result, _chk_offsets) + +# NOTE: same offsets as md5_crypt +_chk_offsets = ( + 12,6,0, + 13,7,1, + 14,8,2, + 15,9,3, + 5,10,4, + 11, +) + +#============================================================================= +# handler +#============================================================================= +class sun_md5_crypt(uh.HasRounds, uh.HasSalt, uh.GenericHandler): + """This class implements the Sun-MD5-Crypt password hash, and follows the :ref:`password-hash-api`. + + It supports a variable-length salt, and a variable number of rounds. + + The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: + + :type salt: str + :param salt: + Optional salt string. + If not specified, a salt will be autogenerated (this is recommended). + If specified, it must be drawn from the regexp range ``[./0-9A-Za-z]``. + + :type salt_size: int + :param salt_size: + If no salt is specified, this parameter can be used to specify + the size (in characters) of the autogenerated salt. + It currently defaults to 8. + + :type rounds: int + :param rounds: + Optional number of rounds to use. + Defaults to 34000, must be between 0 and 4294963199, inclusive. + + :type bare_salt: bool + :param bare_salt: + Optional flag used to enable an alternate salt digest behavior + used by some hash strings in this scheme. + This flag can be ignored by most users. + Defaults to ``False``. + (see :ref:`smc-bare-salt` for details). + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include ``rounds`` + that are too small or too large, and ``salt`` strings that are too long. + + .. versionadded:: 1.6 + """ + #=================================================================== + # class attrs + #=================================================================== + name = "sun_md5_crypt" + setting_kwds = ("salt", "rounds", "bare_salt", "salt_size") + checksum_chars = uh.HASH64_CHARS + checksum_size = 22 + + # NOTE: docs say max password length is 255. + # release 9u2 + + # NOTE: not sure if original crypt has a salt size limit, + # all instances that have been seen use 8 chars. + default_salt_size = 8 + max_salt_size = None + salt_chars = uh.HASH64_CHARS + + default_rounds = 34000 # current passlib default + min_rounds = 0 + max_rounds = 4294963199 ##2**32-1-4096 + # XXX: ^ not sure what it does if past this bound... does 32 int roll over? + rounds_cost = "linear" + + ident_values = (u("$md5$"), u("$md5,")) + + #=================================================================== + # instance attrs + #=================================================================== + bare_salt = False # flag to indicate legacy hashes that lack "$$" suffix + + #=================================================================== + # constructor + #=================================================================== + def __init__(self, bare_salt=False, **kwds): + self.bare_salt = bare_salt + super(sun_md5_crypt, self).__init__(**kwds) + + #=================================================================== + # internal helpers + #=================================================================== + @classmethod + def identify(cls, hash): + hash = uh.to_unicode_for_identify(hash) + return hash.startswith(cls.ident_values) + + @classmethod + def from_string(cls, hash): + hash = to_unicode(hash, "ascii", "hash") + + # + # detect if hash specifies rounds value. + # if so, parse and validate it. + # by end, set 'rounds' to int value, and 'tail' containing salt+chk + # + if hash.startswith(u("$md5$")): + rounds = 0 + salt_idx = 5 + elif hash.startswith(u("$md5,rounds=")): + idx = hash.find(u("$"), 12) + if idx == -1: + raise uh.exc.MalformedHashError(cls, "unexpected end of rounds") + rstr = hash[12:idx] + try: + rounds = int(rstr) + except ValueError: + raise uh.exc.MalformedHashError(cls, "bad rounds") + if rstr != unicode(rounds): + raise uh.exc.ZeroPaddedRoundsError(cls) + if rounds == 0: + # NOTE: not sure if this is forbidden by spec or not; + # but allowing it would complicate things, + # and it should never occur anyways. + raise uh.exc.MalformedHashError(cls, "explicit zero rounds") + salt_idx = idx+1 + else: + raise uh.exc.InvalidHashError(cls) + + # + # salt/checksum separation is kinda weird, + # to deal cleanly with some backward-compatible workarounds + # implemented by original implementation. + # + chk_idx = hash.rfind(u("$"), salt_idx) + if chk_idx == -1: + # ''-config for $-hash + salt = hash[salt_idx:] + chk = None + bare_salt = True + elif chk_idx == len(hash)-1: + if chk_idx > salt_idx and hash[-2] == u("$"): + raise uh.exc.MalformedHashError(cls, "too many '$' separators") + # $-config for $$-hash + salt = hash[salt_idx:-1] + chk = None + bare_salt = False + elif chk_idx > 0 and hash[chk_idx-1] == u("$"): + # $$-hash + salt = hash[salt_idx:chk_idx-1] + chk = hash[chk_idx+1:] + bare_salt = False + else: + # $-hash + salt = hash[salt_idx:chk_idx] + chk = hash[chk_idx+1:] + bare_salt = True + + return cls( + rounds=rounds, + salt=salt, + checksum=chk, + bare_salt=bare_salt, + ) + + def to_string(self, _withchk=True): + ss = u('') if self.bare_salt else u('$') + rounds = self.rounds + if rounds > 0: + hash = u("$md5,rounds=%d$%s%s") % (rounds, self.salt, ss) + else: + hash = u("$md5$%s%s") % (self.salt, ss) + if _withchk: + chk = self.checksum + hash = u("%s$%s") % (hash, chk) + return uascii_to_str(hash) + + #=================================================================== + # primary interface + #=================================================================== + # TODO: if we're on solaris, check for native crypt() support. + # this will require extra testing, to make sure native crypt + # actually behaves correctly. of particular importance: + # when using ""-config, make sure to append "$x" to string. + + def _calc_checksum(self, secret): + # NOTE: no reference for how sun_md5_crypt handles unicode + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + config = str_to_bascii(self.to_string(_withchk=False)) + return raw_sun_md5_crypt(secret, self.rounds, config).decode("ascii") + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/handlers/windows.py b/ansible/lib/python3.11/site-packages/passlib/handlers/windows.py new file mode 100644 index 000000000..e17beba4f --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/handlers/windows.py @@ -0,0 +1,334 @@ +"""passlib.handlers.nthash - Microsoft Windows -related hashes""" +#============================================================================= +# imports +#============================================================================= +# core +from binascii import hexlify +import logging; log = logging.getLogger(__name__) +from warnings import warn +# site +# pkg +from passlib.utils import to_unicode, right_pad_string +from passlib.utils.compat import unicode +from passlib.crypto.digest import lookup_hash +md4 = lookup_hash("md4").const +import passlib.utils.handlers as uh +# local +__all__ = [ + "lmhash", + "nthash", + "bsd_nthash", + "msdcc", + "msdcc2", +] + +#============================================================================= +# lanman hash +#============================================================================= +class lmhash(uh.TruncateMixin, uh.HasEncodingContext, uh.StaticHandler): + """This class implements the Lan Manager Password hash, and follows the :ref:`password-hash-api`. + + It has no salt and a single fixed round. + + The :meth:`~passlib.ifc.PasswordHash.using` method accepts a single + optional keyword: + + :param bool truncate_error: + By default, this will silently truncate passwords larger than 14 bytes. + Setting ``truncate_error=True`` will cause :meth:`~passlib.ifc.PasswordHash.hash` + to raise a :exc:`~passlib.exc.PasswordTruncateError` instead. + + .. versionadded:: 1.7 + + The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.verify` methods accept a single + optional keyword: + + :type encoding: str + :param encoding: + + This specifies what character encoding LMHASH should use when + calculating digest. It defaults to ``cp437``, the most + common encoding encountered. + + Note that while this class outputs digests in lower-case hexadecimal, + it will accept upper-case as well. + """ + #=================================================================== + # class attrs + #=================================================================== + + #-------------------- + # PasswordHash + #-------------------- + name = "lmhash" + setting_kwds = ("truncate_error",) + + #-------------------- + # GenericHandler + #-------------------- + checksum_chars = uh.HEX_CHARS + checksum_size = 32 + + #-------------------- + # TruncateMixin + #-------------------- + truncate_size = 14 + + #-------------------- + # custom + #-------------------- + default_encoding = "cp437" + + #=================================================================== + # methods + #=================================================================== + @classmethod + def _norm_hash(cls, hash): + return hash.lower() + + def _calc_checksum(self, secret): + # check for truncation (during .hash() calls only) + if self.use_defaults: + self._check_truncate_policy(secret) + + return hexlify(self.raw(secret, self.encoding)).decode("ascii") + + # magic constant used by LMHASH + _magic = b"KGS!@#$%" + + @classmethod + def raw(cls, secret, encoding=None): + """encode password using LANMAN hash algorithm. + + :type secret: unicode or utf-8 encoded bytes + :arg secret: secret to hash + :type encoding: str + :arg encoding: + optional encoding to use for unicode inputs. + this defaults to ``cp437``, which is the + common case for most situations. + + :returns: returns string of raw bytes + """ + if not encoding: + encoding = cls.default_encoding + # some nice empircal data re: different encodings is at... + # http://www.openwall.com/lists/john-dev/2011/08/01/2 + # http://www.freerainbowtables.com/phpBB3/viewtopic.php?t=387&p=12163 + from passlib.crypto.des import des_encrypt_block + MAGIC = cls._magic + if isinstance(secret, unicode): + # perform uppercasing while we're still unicode, + # to give a better shot at getting non-ascii chars right. + # (though some codepages do NOT upper-case the same as unicode). + secret = secret.upper().encode(encoding) + elif isinstance(secret, bytes): + # FIXME: just trusting ascii upper will work? + # and if not, how to do codepage specific case conversion? + # we could decode first using , + # but *that* might not always be right. + secret = secret.upper() + else: + raise TypeError("secret must be unicode or bytes") + secret = right_pad_string(secret, 14) + return des_encrypt_block(secret[0:7], MAGIC) + \ + des_encrypt_block(secret[7:14], MAGIC) + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# ntlm hash +#============================================================================= +class nthash(uh.StaticHandler): + """This class implements the NT Password hash, and follows the :ref:`password-hash-api`. + + It has no salt and a single fixed round. + + The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept no optional keywords. + + Note that while this class outputs lower-case hexadecimal digests, + it will accept upper-case digests as well. + """ + #=================================================================== + # class attrs + #=================================================================== + name = "nthash" + checksum_chars = uh.HEX_CHARS + checksum_size = 32 + + #=================================================================== + # methods + #=================================================================== + @classmethod + def _norm_hash(cls, hash): + return hash.lower() + + def _calc_checksum(self, secret): + return hexlify(self.raw(secret)).decode("ascii") + + @classmethod + def raw(cls, secret): + """encode password using MD4-based NTHASH algorithm + + :arg secret: secret as unicode or utf-8 encoded bytes + + :returns: returns string of raw bytes + """ + secret = to_unicode(secret, "utf-8", param="secret") + # XXX: found refs that say only first 128 chars are used. + return md4(secret.encode("utf-16-le")).digest() + + @classmethod + def raw_nthash(cls, secret, hex=False): + warn("nthash.raw_nthash() is deprecated, and will be removed " + "in Passlib 1.8, please use nthash.raw() instead", + DeprecationWarning) + ret = nthash.raw(secret) + return hexlify(ret).decode("ascii") if hex else ret + + #=================================================================== + # eoc + #=================================================================== + +bsd_nthash = uh.PrefixWrapper("bsd_nthash", nthash, prefix="$3$$", ident="$3$$", + doc="""The class support FreeBSD's representation of NTHASH + (which is compatible with the :ref:`modular-crypt-format`), + and follows the :ref:`password-hash-api`. + + It has no salt and a single fixed round. + + The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept no optional keywords. + """) + +##class ntlm_pair(object): +## "combined lmhash & nthash" +## name = "ntlm_pair" +## setting_kwds = () +## _hash_regex = re.compile(u"^(?P[0-9a-f]{32}):(?P[0-9][a-f]{32})$", +## re.I) +## +## @classmethod +## def identify(cls, hash): +## hash = to_unicode(hash, "latin-1", "hash") +## return len(hash) == 65 and cls._hash_regex.match(hash) is not None +## +## @classmethod +## def hash(cls, secret, config=None): +## if config is not None and not cls.identify(config): +## raise uh.exc.InvalidHashError(cls) +## return lmhash.hash(secret) + ":" + nthash.hash(secret) +## +## @classmethod +## def verify(cls, secret, hash): +## hash = to_unicode(hash, "ascii", "hash") +## m = cls._hash_regex.match(hash) +## if not m: +## raise uh.exc.InvalidHashError(cls) +## lm, nt = m.group("lm", "nt") +## # NOTE: verify against both in case encoding issue +## # causes one not to match. +## return lmhash.verify(secret, lm) or nthash.verify(secret, nt) + +#============================================================================= +# msdcc v1 +#============================================================================= +class msdcc(uh.HasUserContext, uh.StaticHandler): + """This class implements Microsoft's Domain Cached Credentials password hash, + and follows the :ref:`password-hash-api`. + + It has a fixed number of rounds, and uses the associated + username as the salt. + + The :meth:`~passlib.ifc.PasswordHash.hash`, :meth:`~passlib.ifc.PasswordHash.genhash`, and :meth:`~passlib.ifc.PasswordHash.verify` methods + have the following optional keywords: + + :type user: str + :param user: + String containing name of user account this password is associated with. + This is required to properly calculate the hash. + + This keyword is case-insensitive, and should contain just the username + (e.g. ``Administrator``, not ``SOMEDOMAIN\\Administrator``). + + Note that while this class outputs lower-case hexadecimal digests, + it will accept upper-case digests as well. + """ + name = "msdcc" + checksum_chars = uh.HEX_CHARS + checksum_size = 32 + + @classmethod + def _norm_hash(cls, hash): + return hash.lower() + + def _calc_checksum(self, secret): + return hexlify(self.raw(secret, self.user)).decode("ascii") + + @classmethod + def raw(cls, secret, user): + """encode password using mscash v1 algorithm + + :arg secret: secret as unicode or utf-8 encoded bytes + :arg user: username to use as salt + + :returns: returns string of raw bytes + """ + secret = to_unicode(secret, "utf-8", param="secret").encode("utf-16-le") + user = to_unicode(user, "utf-8", param="user").lower().encode("utf-16-le") + return md4(md4(secret).digest() + user).digest() + +#============================================================================= +# msdcc2 aka mscash2 +#============================================================================= +class msdcc2(uh.HasUserContext, uh.StaticHandler): + """This class implements version 2 of Microsoft's Domain Cached Credentials + password hash, and follows the :ref:`password-hash-api`. + + It has a fixed number of rounds, and uses the associated + username as the salt. + + The :meth:`~passlib.ifc.PasswordHash.hash`, :meth:`~passlib.ifc.PasswordHash.genhash`, and :meth:`~passlib.ifc.PasswordHash.verify` methods + have the following extra keyword: + + :type user: str + :param user: + String containing name of user account this password is associated with. + This is required to properly calculate the hash. + + This keyword is case-insensitive, and should contain just the username + (e.g. ``Administrator``, not ``SOMEDOMAIN\\Administrator``). + """ + name = "msdcc2" + checksum_chars = uh.HEX_CHARS + checksum_size = 32 + + @classmethod + def _norm_hash(cls, hash): + return hash.lower() + + def _calc_checksum(self, secret): + return hexlify(self.raw(secret, self.user)).decode("ascii") + + @classmethod + def raw(cls, secret, user): + """encode password using msdcc v2 algorithm + + :type secret: unicode or utf-8 bytes + :arg secret: secret + + :type user: str + :arg user: username to use as salt + + :returns: returns string of raw bytes + """ + from passlib.crypto.digest import pbkdf2_hmac + secret = to_unicode(secret, "utf-8", param="secret").encode("utf-16-le") + user = to_unicode(user, "utf-8", param="user").lower().encode("utf-16-le") + tmp = md4(md4(secret).digest() + user).digest() + return pbkdf2_hmac("sha1", tmp, user, 10240, 16) + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/hash.py b/ansible/lib/python3.11/site-packages/passlib/hash.py new file mode 100644 index 000000000..2cc0628da --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/hash.py @@ -0,0 +1,68 @@ +""" +passlib.hash - proxy object mapping hash scheme names -> handlers + +================== +***** NOTICE ***** +================== + +This module does not actually contain any hashes. This file +is a stub that replaces itself with a proxy object. + +This proxy object (passlib.registry._PasslibRegistryProxy) +handles lazy-loading hashes as they are requested. + +The actual implementation of the various hashes is store elsewhere, +mainly in the submodules of the ``passlib.handlers`` subpackage. +""" + +#============================================================================= +# import proxy object and replace this module +#============================================================================= + +# XXX: if any platform has problem w/ lazy modules, could support 'non-lazy' +# version which just imports all schemes known to list_crypt_handlers() + +from passlib.registry import _proxy +import sys +sys.modules[__name__] = _proxy + +#============================================================================= +# HACK: the following bit of code is unreachable, but it's presence seems to +# help make autocomplete work for certain IDEs such as PyCharm. +# this list is automatically regenerated using $SOURCE/admin/regen.py +#============================================================================= + +#---------------------------------------------------- +# begin autocomplete hack (autogenerated 2016-11-10) +#---------------------------------------------------- +if False: + from passlib.handlers.argon2 import argon2 + from passlib.handlers.bcrypt import bcrypt, bcrypt_sha256 + from passlib.handlers.cisco import cisco_asa, cisco_pix, cisco_type7 + from passlib.handlers.des_crypt import bigcrypt, bsdi_crypt, crypt16, des_crypt + from passlib.handlers.digests import hex_md4, hex_md5, hex_sha1, hex_sha256, hex_sha512, htdigest + from passlib.handlers.django import django_bcrypt, django_bcrypt_sha256, django_des_crypt, django_disabled, django_pbkdf2_sha1, django_pbkdf2_sha256, django_salted_md5, django_salted_sha1 + from passlib.handlers.fshp import fshp + from passlib.handlers.ldap_digests import ldap_bcrypt, ldap_bsdi_crypt, ldap_des_crypt, ldap_md5, ldap_md5_crypt, ldap_plaintext, ldap_salted_md5, ldap_salted_sha1, ldap_salted_sha256, ldap_salted_sha512, ldap_sha1, ldap_sha1_crypt, ldap_sha256_crypt, ldap_sha512_crypt + from passlib.handlers.md5_crypt import apr_md5_crypt, md5_crypt + from passlib.handlers.misc import plaintext, unix_disabled, unix_fallback + from passlib.handlers.mssql import mssql2000, mssql2005 + from passlib.handlers.mysql import mysql323, mysql41 + from passlib.handlers.oracle import oracle10, oracle11 + from passlib.handlers.pbkdf2 import atlassian_pbkdf2_sha1, cta_pbkdf2_sha1, dlitz_pbkdf2_sha1, grub_pbkdf2_sha512, ldap_pbkdf2_sha1, ldap_pbkdf2_sha256, ldap_pbkdf2_sha512, pbkdf2_sha1, pbkdf2_sha256, pbkdf2_sha512 + from passlib.handlers.phpass import phpass + from passlib.handlers.postgres import postgres_md5 + from passlib.handlers.roundup import ldap_hex_md5, ldap_hex_sha1, roundup_plaintext + from passlib.handlers.scram import scram + from passlib.handlers.scrypt import scrypt + from passlib.handlers.sha1_crypt import sha1_crypt + from passlib.handlers.sha2_crypt import sha256_crypt, sha512_crypt + from passlib.handlers.sun_md5_crypt import sun_md5_crypt + from passlib.handlers.windows import bsd_nthash, lmhash, msdcc, msdcc2, nthash +#---------------------------------------------------- +# end autocomplete hack +#---------------------------------------------------- + +#============================================================================= +# eoc +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/hosts.py b/ansible/lib/python3.11/site-packages/passlib/hosts.py new file mode 100644 index 000000000..1f137a260 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/hosts.py @@ -0,0 +1,106 @@ +"""passlib.hosts""" +#============================================================================= +# imports +#============================================================================= +# core +from warnings import warn +# pkg +from passlib.context import LazyCryptContext +from passlib.exc import PasslibRuntimeWarning +from passlib import registry +from passlib.utils import has_crypt, unix_crypt_schemes +# local +__all__ = [ + "linux_context", "linux2_context", + "openbsd_context", + "netbsd_context", + "freebsd_context", + "host_context", +] + +#============================================================================= +# linux support +#============================================================================= + +# known platform names - linux2 + +linux_context = linux2_context = LazyCryptContext( + schemes = [ "sha512_crypt", "sha256_crypt", "md5_crypt", + "des_crypt", "unix_disabled" ], + deprecated = [ "des_crypt" ], + ) + +#============================================================================= +# bsd support +#============================================================================= + +# known platform names - +# freebsd2 +# freebsd3 +# freebsd4 +# freebsd5 +# freebsd6 +# freebsd7 +# +# netbsd1 + +# referencing source via -http://fxr.googlebit.com +# freebsd 6,7,8 - des, md5, bcrypt, bsd_nthash +# netbsd - des, ext, md5, bcrypt, sha1 +# openbsd - des, ext, md5, bcrypt + +freebsd_context = LazyCryptContext(["bcrypt", "md5_crypt", "bsd_nthash", + "des_crypt", "unix_disabled"]) + +openbsd_context = LazyCryptContext(["bcrypt", "md5_crypt", "bsdi_crypt", + "des_crypt", "unix_disabled"]) + +netbsd_context = LazyCryptContext(["bcrypt", "sha1_crypt", "md5_crypt", + "bsdi_crypt", "des_crypt", "unix_disabled"]) + +# XXX: include darwin in this list? it's got a BSD crypt variant, +# but that's not what it uses for user passwords. + +#============================================================================= +# current host +#============================================================================= +if registry.os_crypt_present: + # NOTE: this is basically mimicing the output of os crypt(), + # except that it uses passlib's (usually stronger) defaults settings, + # and can be inspected and used much more flexibly. + + def _iter_os_crypt_schemes(): + """helper which iterates over supported os_crypt schemes""" + out = registry.get_supported_os_crypt_schemes() + if out: + # only offer disabled handler if there's another scheme in front, + # as this can't actually hash any passwords + out += ("unix_disabled",) + return out + + host_context = LazyCryptContext(_iter_os_crypt_schemes()) + +#============================================================================= +# other platforms +#============================================================================= + +# known platform strings - +# aix3 +# aix4 +# atheos +# beos5 +# darwin +# generic +# hp-ux11 +# irix5 +# irix6 +# mac +# next3 +# os2emx +# riscos +# sunos5 +# unixware7 + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/ifc.py b/ansible/lib/python3.11/site-packages/passlib/ifc.py new file mode 100644 index 000000000..559d256e3 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/ifc.py @@ -0,0 +1,353 @@ +"""passlib.ifc - abstract interfaces used by Passlib""" +#============================================================================= +# imports +#============================================================================= +# core +import logging; log = logging.getLogger(__name__) +import sys +# site +# pkg +from passlib.utils.decor import deprecated_method +# local +__all__ = [ + "PasswordHash", +] + +#============================================================================= +# 2/3 compatibility helpers +#============================================================================= +def recreate_with_metaclass(meta): + """class decorator that re-creates class using metaclass""" + def builder(cls): + if meta is type(cls): + return cls + return meta(cls.__name__, cls.__bases__, cls.__dict__.copy()) + return builder + +#============================================================================= +# PasswordHash interface +#============================================================================= +from abc import ABCMeta, abstractmethod, abstractproperty + +# TODO: make this actually use abstractproperty(), +# now that we dropped py25, 'abc' is always available. + +# XXX: rename to PasswordHasher? + +@recreate_with_metaclass(ABCMeta) +class PasswordHash(object): + """This class describes an abstract interface which all password hashes + in Passlib adhere to. Under Python 2.6 and up, this is an actual + Abstract Base Class built using the :mod:`!abc` module. + + See the Passlib docs for full documentation. + """ + #=================================================================== + # class attributes + #=================================================================== + + #--------------------------------------------------------------- + # general information + #--------------------------------------------------------------- + ##name + ##setting_kwds + ##context_kwds + + #: flag which indicates this hasher matches a "disabled" hash + #: (e.g. unix_disabled, or django_disabled); and doesn't actually + #: depend on the provided password. + is_disabled = False + + #: Should be None, or a positive integer indicating hash + #: doesn't support secrets larger than this value. + #: Whether hash throws error or silently truncates secret + #: depends on .truncate_error and .truncate_verify_reject flags below. + #: NOTE: calls may treat as boolean, since value will never be 0. + #: .. versionadded:: 1.7 + #: .. TODO: passlib 1.8: deprecate/rename this attr to "max_secret_size"? + truncate_size = None + + # NOTE: these next two default to the optimistic "ideal", + # most hashes in passlib have to default to False + # for backward compat and/or expected behavior with existing hashes. + + #: If True, .hash() should throw a :exc:`~passlib.exc.PasswordSizeError` for + #: any secrets larger than .truncate_size. Many hashers default to False + #: for historical / compatibility purposes, indicating they will silently + #: truncate instead. All such hashers SHOULD support changing + #: the policy via ``.using(truncate_error=True)``. + #: .. versionadded:: 1.7 + #: .. TODO: passlib 1.8: deprecate/rename this attr to "truncate_hash_error"? + truncate_error = True + + #: If True, .verify() should reject secrets larger than max_password_size. + #: Many hashers default to False for historical / compatibility purposes, + #: indicating they will match on the truncated portion instead. + #: .. versionadded:: 1.7.1 + truncate_verify_reject = True + + #--------------------------------------------------------------- + # salt information -- if 'salt' in setting_kwds + #--------------------------------------------------------------- + ##min_salt_size + ##max_salt_size + ##default_salt_size + ##salt_chars + ##default_salt_chars + + #--------------------------------------------------------------- + # rounds information -- if 'rounds' in setting_kwds + #--------------------------------------------------------------- + ##min_rounds + ##max_rounds + ##default_rounds + ##rounds_cost + + #--------------------------------------------------------------- + # encoding info -- if 'encoding' in context_kwds + #--------------------------------------------------------------- + ##default_encoding + + #=================================================================== + # primary methods + #=================================================================== + @classmethod + @abstractmethod + def hash(cls, secret, # * + **setting_and_context_kwds): # pragma: no cover -- abstract method + r""" + Hash secret, returning result. + Should handle generating salt, etc, and should return string + containing identifier, salt & other configuration, as well as digest. + + :param \\*\\*settings_kwds: + + Pass in settings to customize configuration of resulting hash. + + .. deprecated:: 1.7 + + Starting with Passlib 1.7, callers should no longer pass settings keywords + (e.g. ``rounds`` or ``salt`` directly to :meth:`!hash`); should use + ``.using(**settings).hash(secret)`` construction instead. + + Support will be removed in Passlib 2.0. + + :param \\*\\*context_kwds: + + Specific algorithms may require context-specific information (such as the user login). + """ + # FIXME: need stub for classes that define .encrypt() instead ... + # this should call .encrypt(), and check for recursion back to here. + raise NotImplementedError("must be implemented by subclass") + + @deprecated_method(deprecated="1.7", removed="2.0", replacement=".hash()") + @classmethod + def encrypt(cls, *args, **kwds): + """ + Legacy alias for :meth:`hash`. + + .. deprecated:: 1.7 + This method was renamed to :meth:`!hash` in version 1.7. + This alias will be removed in version 2.0, and should only + be used for compatibility with Passlib 1.3 - 1.6. + """ + return cls.hash(*args, **kwds) + + # XXX: could provide default implementation which hands value to + # hash(), and then does constant-time comparision on the result + # (after making both are same string type) + @classmethod + @abstractmethod + def verify(cls, secret, hash, **context_kwds): # pragma: no cover -- abstract method + """verify secret against hash, returns True/False""" + raise NotImplementedError("must be implemented by subclass") + + #=================================================================== + # configuration + #=================================================================== + @classmethod + @abstractmethod + def using(cls, relaxed=False, **kwds): + """ + Return another hasher object (typically a subclass of the current one), + which integrates the configuration options specified by ``kwds``. + This should *always* return a new object, even if no configuration options are changed. + + .. todo:: + + document which options are accepted. + + :returns: + typically returns a subclass for most hasher implementations. + + .. todo:: + + add this method to main documentation. + """ + raise NotImplementedError("must be implemented by subclass") + + #=================================================================== + # migration + #=================================================================== + @classmethod + def needs_update(cls, hash, secret=None): + """ + check if hash's configuration is outside desired bounds, + or contains some other internal option which requires + updating the password hash. + + :param hash: + hash string to examine + + :param secret: + optional secret known to have verified against the provided hash. + (this is used by some hashes to detect legacy algorithm mistakes). + + :return: + whether secret needs re-hashing. + + .. versionadded:: 1.7 + """ + # by default, always report that we don't need update + return False + + #=================================================================== + # additional methods + #=================================================================== + @classmethod + @abstractmethod + def identify(cls, hash): # pragma: no cover -- abstract method + """check if hash belongs to this scheme, returns True/False""" + raise NotImplementedError("must be implemented by subclass") + + @deprecated_method(deprecated="1.7", removed="2.0") + @classmethod + def genconfig(cls, **setting_kwds): # pragma: no cover -- abstract method + """ + compile settings into a configuration string for genhash() + + .. deprecated:: 1.7 + + As of 1.7, this method is deprecated, and slated for complete removal in Passlib 2.0. + + For all known real-world uses, hashing a constant string + should provide equivalent functionality. + + This deprecation may be reversed if a use-case presents itself in the mean time. + """ + # NOTE: this fallback runs full hash alg, w/ whatever cost param is passed along. + # implementations (esp ones w/ variable cost) will want to subclass this + # with a constant-time implementation that just renders a config string. + if cls.context_kwds: + raise NotImplementedError("must be implemented by subclass") + return cls.using(**setting_kwds).hash("") + + @deprecated_method(deprecated="1.7", removed="2.0") + @classmethod + def genhash(cls, secret, config, **context): + """ + generated hash for secret, using settings from config/hash string + + .. deprecated:: 1.7 + + As of 1.7, this method is deprecated, and slated for complete removal in Passlib 2.0. + + This deprecation may be reversed if a use-case presents itself in the mean time. + """ + # XXX: if hashes reliably offered a .parse() method, could make a fallback for this. + raise NotImplementedError("must be implemented by subclass") + + #=================================================================== + # undocumented methods / attributes + #=================================================================== + # the following entry points are used internally by passlib, + # and aren't documented as part of the exposed interface. + # they are subject to change between releases, + # but are documented here so there's a list of them *somewhere*. + + #--------------------------------------------------------------- + # extra metdata + #--------------------------------------------------------------- + + #: this attribute shouldn't be used by hashers themselves, + #: it's reserved for the CryptContext to track which hashers are deprecated. + #: Note the context will only set this on objects it owns (and generated by .using()), + #: and WONT set it on global objects. + #: [added in 1.7] + #: TODO: document this, or at least the use of testing for + #: 'CryptContext().handler().deprecated' + deprecated = False + + #: optionally present if hasher corresponds to format built into Django. + #: this attribute (if not None) should be the Django 'algorithm' name. + #: also indicates to passlib.ext.django that (when installed in django), + #: django's native hasher should be used in preference to this one. + ## django_name + + #--------------------------------------------------------------- + # checksum information - defined for many hashes + #--------------------------------------------------------------- + ## checksum_chars + ## checksum_size + + #--------------------------------------------------------------- + # experimental methods + #--------------------------------------------------------------- + + ##@classmethod + ##def normhash(cls, hash): + ## """helper to clean up non-canonic instances of hash. + ## currently only provided by bcrypt() to fix an historical passlib issue. + ## """ + + # experimental helper to parse hash into components. + ##@classmethod + ##def parsehash(cls, hash, checksum=True, sanitize=False): + ## """helper to parse hash into components, returns dict""" + + # experiment helper to estimate bitsize of different hashes, + # implement for GenericHandler, but may be currently be off for some hashes. + # want to expand this into a way to programmatically compare + # "strengths" of different hashes and hash algorithms. + # still needs to have some factor for estimate relative cost per round, + # ala in the style of the scrypt whitepaper. + ##@classmethod + ##def bitsize(cls, **kwds): + ## """returns dict mapping component -> bits contributed. + ## components currently include checksum, salt, rounds. + ## """ + + #=================================================================== + # eoc + #=================================================================== + +class DisabledHash(PasswordHash): + """ + extended disabled-hash methods; only need be present if .disabled = True + """ + + is_disabled = True + + @classmethod + def disable(cls, hash=None): + """ + return string representing a 'disabled' hash; + optionally including previously enabled hash + (this is up to the individual scheme). + """ + # default behavior: ignore original hash, return standalone marker + return cls.hash("") + + @classmethod + def enable(cls, hash): + """ + given a disabled-hash string, + extract previously-enabled hash if one is present, + otherwise raises ValueError + """ + # default behavior: no way to restore original hash + raise ValueError("cannot restore original hash") + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/pwd.py b/ansible/lib/python3.11/site-packages/passlib/pwd.py new file mode 100644 index 000000000..27ed228bb --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/pwd.py @@ -0,0 +1,809 @@ +"""passlib.pwd -- password generation helpers""" +#============================================================================= +# imports +#============================================================================= +from __future__ import absolute_import, division, print_function, unicode_literals +# core +import codecs +from collections import defaultdict +try: + from collections.abc import MutableMapping +except ImportError: + # py2 compat + from collections import MutableMapping +from math import ceil, log as logf +import logging; log = logging.getLogger(__name__) +import pkg_resources +import os +# site +# pkg +from passlib import exc +from passlib.utils.compat import PY2, irange, itervalues, int_types +from passlib.utils import rng, getrandstr, to_unicode +from passlib.utils.decor import memoized_property +# local +__all__ = [ + "genword", "default_charsets", + "genphrase", "default_wordsets", +] + +#============================================================================= +# constants +#============================================================================= + +# XXX: rename / publically document this map? +entropy_aliases = dict( + # barest protection from throttled online attack + unsafe=12, + + # some protection from unthrottled online attack + weak=24, + + # some protection from offline attacks + fair=36, + + # reasonable protection from offline attacks + strong=48, + + # very good protection from offline attacks + secure=60, +) + +#============================================================================= +# internal helpers +#============================================================================= + +def _superclasses(obj, cls): + """return remaining classes in object's MRO after cls""" + mro = type(obj).__mro__ + return mro[mro.index(cls)+1:] + + +def _self_info_rate(source): + """ + returns 'rate of self-information' -- + i.e. average (per-symbol) entropy of the sequence **source**, + where probability of a given symbol occurring is calculated based on + the number of occurrences within the sequence itself. + + if all elements of the source are unique, this should equal ``log(len(source), 2)``. + + :arg source: + iterable containing 0+ symbols + (e.g. list of strings or ints, string of characters, etc). + + :returns: + float bits of entropy + """ + try: + size = len(source) + except TypeError: + # if len() doesn't work, calculate size by summing counts later + size = None + counts = defaultdict(int) + for char in source: + counts[char] += 1 + if size is None: + values = counts.values() + size = sum(values) + else: + values = itervalues(counts) + if not size: + return 0 + # NOTE: the following performs ``- sum(value / size * logf(value / size, 2) for value in values)``, + # it just does so with as much pulled out of the sum() loop as possible... + return logf(size, 2) - sum(value * logf(value, 2) for value in values) / size + + +# def _total_self_info(source): +# """ +# return total self-entropy of a sequence +# (the average entropy per symbol * size of sequence) +# """ +# return _self_info_rate(source) * len(source) + + +def _open_asset_path(path, encoding=None): + """ + :param asset_path: + string containing absolute path to file, + or package-relative path using format + ``"python.module:relative/file/path"``. + + :returns: + filehandle opened in 'rb' mode + (unless encoding explicitly specified) + """ + if encoding: + return codecs.getreader(encoding)(_open_asset_path(path)) + if os.path.isabs(path): + return open(path, "rb") + package, sep, subpath = path.partition(":") + if not sep: + raise ValueError("asset path must be absolute file path " + "or use 'pkg.name:sub/path' format: %r" % (path,)) + return pkg_resources.resource_stream(package, subpath) + + +#: type aliases +_sequence_types = (list, tuple) +_set_types = (set, frozenset) + +#: set of elements that ensure_unique() has validated already. +_ensure_unique_cache = set() + + +def _ensure_unique(source, param="source"): + """ + helper for generators -- + Throws ValueError if source elements aren't unique. + Error message will display (abbreviated) repr of the duplicates in a string/list + """ + # check cache to speed things up for frozensets / tuples / strings + cache = _ensure_unique_cache + hashable = True + try: + if source in cache: + return True + except TypeError: + hashable = False + + # check if it has dup elements + if isinstance(source, _set_types) or len(set(source)) == len(source): + if hashable: + try: + cache.add(source) + except TypeError: + # XXX: under pypy, "list() in set()" above doesn't throw TypeError, + # but trying to add unhashable it to a set *does*. + pass + return True + + # build list of duplicate values + seen = set() + dups = set() + for elem in source: + (dups if elem in seen else seen).add(elem) + dups = sorted(dups) + trunc = 8 + if len(dups) > trunc: + trunc = 5 + dup_repr = ", ".join(repr(str(word)) for word in dups[:trunc]) + if len(dups) > trunc: + dup_repr += ", ... plus %d others" % (len(dups) - trunc) + + # throw error + raise ValueError("`%s` cannot contain duplicate elements: %s" % + (param, dup_repr)) + +#============================================================================= +# base generator class +#============================================================================= +class SequenceGenerator(object): + """ + Base class used by word & phrase generators. + + These objects take a series of options, corresponding + to those of the :func:`generate` function. + They act as callables which can be used to generate a password + or a list of 1+ passwords. They also expose some read-only + informational attributes. + + Parameters + ---------- + :param entropy: + Optionally specify the amount of entropy the resulting passwords + should contain (as measured with respect to the generator itself). + This will be used to auto-calculate the required password size. + + :param length: + Optionally specify the length of password to generate, + measured as count of whatever symbols the subclass uses (characters or words). + Note if ``entropy`` requires a larger minimum length, + that will be used instead. + + :param rng: + Optionally provide a custom RNG source to use. + Should be an instance of :class:`random.Random`, + defaults to :class:`random.SystemRandom`. + + Attributes + ---------- + .. autoattribute:: length + .. autoattribute:: symbol_count + .. autoattribute:: entropy_per_symbol + .. autoattribute:: entropy + + Subclassing + ----------- + Subclasses must implement the ``.__next__()`` method, + and set ``.symbol_count`` before calling base ``__init__`` method. + """ + #============================================================================= + # instance attrs + #============================================================================= + + #: requested size of final password + length = None + + #: requested entropy of final password + requested_entropy = "strong" + + #: random number source to use + rng = rng + + #: number of potential symbols (must be filled in by subclass) + symbol_count = None + + #============================================================================= + # init + #============================================================================= + def __init__(self, entropy=None, length=None, rng=None, **kwds): + + # make sure subclass set things up correctly + assert self.symbol_count is not None, "subclass must set .symbol_count" + + # init length & requested entropy + if entropy is not None or length is None: + if entropy is None: + entropy = self.requested_entropy + entropy = entropy_aliases.get(entropy, entropy) + if entropy <= 0: + raise ValueError("`entropy` must be positive number") + min_length = int(ceil(entropy / self.entropy_per_symbol)) + if length is None or length < min_length: + length = min_length + + self.requested_entropy = entropy + + if length < 1: + raise ValueError("`length` must be positive integer") + self.length = length + + # init other common options + if rng is not None: + self.rng = rng + + # hand off to parent + if kwds and _superclasses(self, SequenceGenerator) == (object,): + raise TypeError("Unexpected keyword(s): %s" % ", ".join(kwds.keys())) + super(SequenceGenerator, self).__init__(**kwds) + + #============================================================================= + # informational helpers + #============================================================================= + + @memoized_property + def entropy_per_symbol(self): + """ + Average entropy per symbol (assuming all symbols have equal probability) + """ + return logf(self.symbol_count, 2) + + @memoized_property + def entropy(self): + """ + Effective entropy of generated passwords. + + This value will always be a multiple of :attr:`entropy_per_symbol`. + If entropy is specified in constructor, :attr:`length` will be chosen so + so that this value is the smallest multiple >= :attr:`requested_entropy`. + """ + return self.length * self.entropy_per_symbol + + #============================================================================= + # generation + #============================================================================= + def __next__(self): + """main generation function, should create one password/phrase""" + raise NotImplementedError("implement in subclass") + + def __call__(self, returns=None): + """ + frontend used by genword() / genphrase() to create passwords + """ + if returns is None: + return next(self) + elif isinstance(returns, int_types): + return [next(self) for _ in irange(returns)] + elif returns is iter: + return self + else: + raise exc.ExpectedTypeError(returns, ", int, or ", "returns") + + def __iter__(self): + return self + + if PY2: + def next(self): + return self.__next__() + + #============================================================================= + # eoc + #============================================================================= + +#============================================================================= +# default charsets +#============================================================================= + +#: global dict of predefined characters sets +default_charsets = dict( + # ascii letters, digits, and some punctuation + ascii_72='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*?/', + + # ascii letters and digits + ascii_62='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', + + # ascii_50, without visually similar '1IiLl', '0Oo', '5S', '8B' + ascii_50='234679abcdefghjkmnpqrstuvwxyzACDEFGHJKMNPQRTUVWXYZ', + + # lower case hexadecimal + hex='0123456789abcdef', +) + +#============================================================================= +# password generator +#============================================================================= + +class WordGenerator(SequenceGenerator): + """ + Class which generates passwords by randomly choosing from a string of unique characters. + + Parameters + ---------- + :param chars: + custom character string to draw from. + + :param charset: + predefined charset to draw from. + + :param \\*\\*kwds: + all other keywords passed to the :class:`SequenceGenerator` parent class. + + Attributes + ---------- + .. autoattribute:: chars + .. autoattribute:: charset + .. autoattribute:: default_charsets + """ + #============================================================================= + # instance attrs + #============================================================================= + + #: Predefined character set in use (set to None for instances using custom 'chars') + charset = "ascii_62" + + #: string of chars to draw from -- usually filled in from charset + chars = None + + #============================================================================= + # init + #============================================================================= + def __init__(self, chars=None, charset=None, **kwds): + + # init chars and charset + if chars: + if charset: + raise TypeError("`chars` and `charset` are mutually exclusive") + else: + if not charset: + charset = self.charset + assert charset + chars = default_charsets[charset] + self.charset = charset + chars = to_unicode(chars, param="chars") + _ensure_unique(chars, param="chars") + self.chars = chars + + # hand off to parent + super(WordGenerator, self).__init__(**kwds) + # log.debug("WordGenerator(): entropy/char=%r", self.entropy_per_symbol) + + #============================================================================= + # informational helpers + #============================================================================= + + @memoized_property + def symbol_count(self): + return len(self.chars) + + #============================================================================= + # generation + #============================================================================= + + def __next__(self): + # XXX: could do things like optionally ensure certain character groups + # (e.g. letters & punctuation) are included + return getrandstr(self.rng, self.chars, self.length) + + #============================================================================= + # eoc + #============================================================================= + + +def genword(entropy=None, length=None, returns=None, **kwds): + """Generate one or more random passwords. + + This function uses :mod:`random.SystemRandom` to generate + one or more passwords using various character sets. + The complexity of the password can be specified + by size, or by the desired amount of entropy. + + Usage Example:: + + >>> # generate a random alphanumeric string with 48 bits of entropy (the default) + >>> from passlib import pwd + >>> pwd.genword() + 'DnBHvDjMK6' + + >>> # generate a random hexadecimal string with 52 bits of entropy + >>> pwd.genword(entropy=52, charset="hex") + '310f1a7ac793f' + + :param entropy: + Strength of resulting password, measured in 'guessing entropy' bits. + An appropriate **length** value will be calculated + based on the requested entropy amount, and the size of the character set. + + This can be a positive integer, or one of the following preset + strings: ``"weak"`` (24), ``"fair"`` (36), + ``"strong"`` (48), and ``"secure"`` (56). + + If neither this or **length** is specified, **entropy** will default + to ``"strong"`` (48). + + :param length: + Size of resulting password, measured in characters. + If omitted, the size is auto-calculated based on the **entropy** parameter. + + If both **entropy** and **length** are specified, + the stronger value will be used. + + :param returns: + Controls what this function returns: + + * If ``None`` (the default), this function will generate a single password. + * If an integer, this function will return a list containing that many passwords. + * If the ``iter`` constant, will return an iterator that yields passwords. + + :param chars: + + Optionally specify custom string of characters to use when randomly + generating a password. This option cannot be combined with **charset**. + + :param charset: + + The predefined character set to draw from (if not specified by **chars**). + There are currently four presets available: + + * ``"ascii_62"`` (the default) -- all digits and ascii upper & lowercase letters. + Provides ~5.95 entropy per character. + + * ``"ascii_50"`` -- subset which excludes visually similar characters + (``1IiLl0Oo5S8B``). Provides ~5.64 entropy per character. + + * ``"ascii_72"`` -- all digits and ascii upper & lowercase letters, + as well as some punctuation. Provides ~6.17 entropy per character. + + * ``"hex"`` -- Lower case hexadecimal. Providers 4 bits of entropy per character. + + :returns: + :class:`!unicode` string containing randomly generated password; + or list of 1+ passwords if :samp:`returns={int}` is specified. + """ + gen = WordGenerator(length=length, entropy=entropy, **kwds) + return gen(returns) + +#============================================================================= +# default wordsets +#============================================================================= + +def _load_wordset(asset_path): + """ + load wordset from compressed datafile within package data. + file should be utf-8 encoded + + :param asset_path: + string containing absolute path to wordset file, + or "python.module:relative/file/path". + + :returns: + tuple of words, as loaded from specified words file. + """ + # open resource file, convert to tuple of words (strip blank lines & ws) + with _open_asset_path(asset_path, "utf-8") as fh: + gen = (word.strip() for word in fh) + words = tuple(word for word in gen if word) + + # NOTE: works but not used + # # detect if file uses " " format, and strip numeric prefix + # def extract(row): + # idx, word = row.replace("\t", " ").split(" ", 1) + # if not idx.isdigit(): + # raise ValueError("row is not dice index + word") + # return word + # try: + # extract(words[-1]) + # except ValueError: + # pass + # else: + # words = tuple(extract(word) for word in words) + + log.debug("loaded %d-element wordset from %r", len(words), asset_path) + return words + + +class WordsetDict(MutableMapping): + """ + Special mapping used to store dictionary of wordsets. + Different from a regular dict in that some wordsets + may be lazy-loaded from an asset path. + """ + + #: dict of key -> asset path + paths = None + + #: dict of key -> value + _loaded = None + + def __init__(self, *args, **kwds): + self.paths = {} + self._loaded = {} + super(WordsetDict, self).__init__(*args, **kwds) + + def __getitem__(self, key): + try: + return self._loaded[key] + except KeyError: + pass + path = self.paths[key] + value = self._loaded[key] = _load_wordset(path) + return value + + def set_path(self, key, path): + """ + set asset path to lazy-load wordset from. + """ + self.paths[key] = path + + def __setitem__(self, key, value): + self._loaded[key] = value + + def __delitem__(self, key): + if key in self: + del self._loaded[key] + self.paths.pop(key, None) + else: + del self.paths[key] + + @property + def _keyset(self): + keys = set(self._loaded) + keys.update(self.paths) + return keys + + def __iter__(self): + return iter(self._keyset) + + def __len__(self): + return len(self._keyset) + + # NOTE: speeds things up, and prevents contains from lazy-loading + def __contains__(self, key): + return key in self._loaded or key in self.paths + + +#: dict of predefined word sets. +#: key is name of wordset, value should be sequence of words. +default_wordsets = WordsetDict() + +# register the wordsets built into passlib +for name in "eff_long eff_short eff_prefixed bip39".split(): + default_wordsets.set_path(name, "passlib:_data/wordsets/%s.txt" % name) + +#============================================================================= +# passphrase generator +#============================================================================= +class PhraseGenerator(SequenceGenerator): + """class which generates passphrases by randomly choosing + from a list of unique words. + + :param wordset: + wordset to draw from. + :param preset: + name of preset wordlist to use instead of ``wordset``. + :param spaces: + whether to insert spaces between words in output (defaults to ``True``). + :param \\*\\*kwds: + all other keywords passed to the :class:`SequenceGenerator` parent class. + + .. autoattribute:: wordset + """ + #============================================================================= + # instance attrs + #============================================================================= + + #: predefined wordset to use + wordset = "eff_long" + + #: list of words to draw from + words = None + + #: separator to use when joining words + sep = " " + + #============================================================================= + # init + #============================================================================= + def __init__(self, wordset=None, words=None, sep=None, **kwds): + + # load wordset + if words is not None: + if wordset is not None: + raise TypeError("`words` and `wordset` are mutually exclusive") + else: + if wordset is None: + wordset = self.wordset + assert wordset + words = default_wordsets[wordset] + self.wordset = wordset + + # init words + if not isinstance(words, _sequence_types): + words = tuple(words) + _ensure_unique(words, param="words") + self.words = words + + # init separator + if sep is None: + sep = self.sep + sep = to_unicode(sep, param="sep") + self.sep = sep + + # hand off to parent + super(PhraseGenerator, self).__init__(**kwds) + ##log.debug("PhraseGenerator(): entropy/word=%r entropy/char=%r min_chars=%r", + ## self.entropy_per_symbol, self.entropy_per_char, self.min_chars) + + #============================================================================= + # informational helpers + #============================================================================= + + @memoized_property + def symbol_count(self): + return len(self.words) + + #============================================================================= + # generation + #============================================================================= + + def __next__(self): + words = (self.rng.choice(self.words) for _ in irange(self.length)) + return self.sep.join(words) + + #============================================================================= + # eoc + #============================================================================= + + +def genphrase(entropy=None, length=None, returns=None, **kwds): + """Generate one or more random password / passphrases. + + This function uses :mod:`random.SystemRandom` to generate + one or more passwords; it can be configured to generate + alphanumeric passwords, or full english phrases. + The complexity of the password can be specified + by size, or by the desired amount of entropy. + + Usage Example:: + + >>> # generate random phrase with 48 bits of entropy + >>> from passlib import pwd + >>> pwd.genphrase() + 'gangly robbing salt shove' + + >>> # generate a random phrase with 52 bits of entropy + >>> # using a particular wordset + >>> pwd.genword(entropy=52, wordset="bip39") + 'wheat dilemma reward rescue diary' + + :param entropy: + Strength of resulting password, measured in 'guessing entropy' bits. + An appropriate **length** value will be calculated + based on the requested entropy amount, and the size of the word set. + + This can be a positive integer, or one of the following preset + strings: ``"weak"`` (24), ``"fair"`` (36), + ``"strong"`` (48), and ``"secure"`` (56). + + If neither this or **length** is specified, **entropy** will default + to ``"strong"`` (48). + + :param length: + Length of resulting password, measured in words. + If omitted, the size is auto-calculated based on the **entropy** parameter. + + If both **entropy** and **length** are specified, + the stronger value will be used. + + :param returns: + Controls what this function returns: + + * If ``None`` (the default), this function will generate a single password. + * If an integer, this function will return a list containing that many passwords. + * If the ``iter`` builtin, will return an iterator that yields passwords. + + :param words: + + Optionally specifies a list/set of words to use when randomly generating a passphrase. + This option cannot be combined with **wordset**. + + :param wordset: + + The predefined word set to draw from (if not specified by **words**). + There are currently four presets available: + + ``"eff_long"`` (the default) + + Wordset containing 7776 english words of ~7 letters. + Constructed by the EFF, it offers ~12.9 bits of entropy per word. + + This wordset (and the other ``"eff_"`` wordsets) + were `created by the EFF `_ + to aid in generating passwords. See their announcement page + for more details about the design & properties of these wordsets. + + ``"eff_short"`` + + Wordset containing 1296 english words of ~4.5 letters. + Constructed by the EFF, it offers ~10.3 bits of entropy per word. + + ``"eff_prefixed"`` + + Wordset containing 1296 english words of ~8 letters, + selected so that they each have a unique 3-character prefix. + Constructed by the EFF, it offers ~10.3 bits of entropy per word. + + ``"bip39"`` + + Wordset of 2048 english words of ~5 letters, + selected so that they each have a unique 4-character prefix. + Published as part of Bitcoin's `BIP 39 `_, + this wordset has exactly 11 bits of entropy per word. + + This list offers words that are typically shorter than ``"eff_long"`` + (at the cost of slightly less entropy); and much shorter than + ``"eff_prefixed"`` (at the cost of a longer unique prefix). + + :param sep: + Optional separator to use when joining words. + Defaults to ``" "`` (a space), but can be an empty string, a hyphen, etc. + + :returns: + :class:`!unicode` string containing randomly generated passphrase; + or list of 1+ passphrases if :samp:`returns={int}` is specified. + """ + gen = PhraseGenerator(entropy=entropy, length=length, **kwds) + return gen(returns) + +#============================================================================= +# strength measurement +# +# NOTE: +# for a little while, had rough draft of password strength measurement alg here. +# but not sure if there's value in yet another measurement algorithm, +# that's not just duplicating the effort of libraries like zxcbn. +# may revive it later, but for now, leaving some refs to others out there: +# * NIST 800-63 has simple alg +# * zxcvbn (https://tech.dropbox.com/2012/04/zxcvbn-realistic-password-strength-estimation/) +# might also be good, and has approach similar to composite approach i was already thinking about, +# but much more well thought out. +# * passfault (https://github.com/c-a-m/passfault) looks thorough, +# but may have licensing issues, plus porting to python looks like very big job :( +# * give a look at running things through zlib - might be able to cheaply +# catch extra redundancies. +#============================================================================= + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/registry.py b/ansible/lib/python3.11/site-packages/passlib/registry.py new file mode 100644 index 000000000..9964b257e --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/registry.py @@ -0,0 +1,547 @@ +"""passlib.registry - registry for password hash handlers""" +#============================================================================= +# imports +#============================================================================= +# core +import re +import logging; log = logging.getLogger(__name__) +from warnings import warn +# pkg +from passlib import exc +from passlib.exc import ExpectedTypeError, PasslibWarning +from passlib.ifc import PasswordHash +from passlib.utils import ( + is_crypt_handler, has_crypt as os_crypt_present, + unix_crypt_schemes as os_crypt_schemes, +) +from passlib.utils.compat import unicode_or_str +from passlib.utils.decor import memoize_single_value +# local +__all__ = [ + "register_crypt_handler_path", + "register_crypt_handler", + "get_crypt_handler", + "list_crypt_handlers", +] + +#============================================================================= +# proxy object used in place of 'passlib.hash' module +#============================================================================= +class _PasslibRegistryProxy(object): + """proxy module passlib.hash + + this module is in fact an object which lazy-loads + the requested password hash algorithm from wherever it has been stored. + it acts as a thin wrapper around :func:`passlib.registry.get_crypt_handler`. + """ + __name__ = "passlib.hash" + __package__ = None + + def __getattr__(self, attr): + if attr.startswith("_"): + raise AttributeError("missing attribute: %r" % (attr,)) + handler = get_crypt_handler(attr, None) + if handler: + return handler + else: + raise AttributeError("unknown password hash: %r" % (attr,)) + + def __setattr__(self, attr, value): + if attr.startswith("_"): + # writing to private attributes should behave normally. + # (required so GAE can write to the __loader__ attribute). + object.__setattr__(self, attr, value) + else: + # writing to public attributes should be treated + # as attempting to register a handler. + register_crypt_handler(value, _attr=attr) + + def __repr__(self): + return "" + + def __dir__(self): + # this adds in lazy-loaded handler names, + # otherwise this is the standard dir() implementation. + attrs = set(dir(self.__class__)) + attrs.update(self.__dict__) + attrs.update(_locations) + return sorted(attrs) + +# create single instance - available publically as 'passlib.hash' +_proxy = _PasslibRegistryProxy() + +#============================================================================= +# internal registry state +#============================================================================= + +# singleton uses to detect omitted keywords +_UNSET = object() + +# dict mapping name -> loaded handlers (just uses proxy object's internal dict) +_handlers = _proxy.__dict__ + +# dict mapping names -> import path for lazy loading. +# * import path should be "module.path" or "module.path:attr" +# * if attr omitted, "name" used as default. +_locations = dict( + # NOTE: this is a hardcoded list of the handlers built into passlib, + # applications should call register_crypt_handler_path() + apr_md5_crypt = "passlib.handlers.md5_crypt", + argon2 = "passlib.handlers.argon2", + atlassian_pbkdf2_sha1 = "passlib.handlers.pbkdf2", + bcrypt = "passlib.handlers.bcrypt", + bcrypt_sha256 = "passlib.handlers.bcrypt", + bigcrypt = "passlib.handlers.des_crypt", + bsd_nthash = "passlib.handlers.windows", + bsdi_crypt = "passlib.handlers.des_crypt", + cisco_pix = "passlib.handlers.cisco", + cisco_asa = "passlib.handlers.cisco", + cisco_type7 = "passlib.handlers.cisco", + cta_pbkdf2_sha1 = "passlib.handlers.pbkdf2", + crypt16 = "passlib.handlers.des_crypt", + des_crypt = "passlib.handlers.des_crypt", + django_argon2 = "passlib.handlers.django", + django_bcrypt = "passlib.handlers.django", + django_bcrypt_sha256 = "passlib.handlers.django", + django_pbkdf2_sha256 = "passlib.handlers.django", + django_pbkdf2_sha1 = "passlib.handlers.django", + django_salted_sha1 = "passlib.handlers.django", + django_salted_md5 = "passlib.handlers.django", + django_des_crypt = "passlib.handlers.django", + django_disabled = "passlib.handlers.django", + dlitz_pbkdf2_sha1 = "passlib.handlers.pbkdf2", + fshp = "passlib.handlers.fshp", + grub_pbkdf2_sha512 = "passlib.handlers.pbkdf2", + hex_md4 = "passlib.handlers.digests", + hex_md5 = "passlib.handlers.digests", + hex_sha1 = "passlib.handlers.digests", + hex_sha256 = "passlib.handlers.digests", + hex_sha512 = "passlib.handlers.digests", + htdigest = "passlib.handlers.digests", + ldap_plaintext = "passlib.handlers.ldap_digests", + ldap_md5 = "passlib.handlers.ldap_digests", + ldap_sha1 = "passlib.handlers.ldap_digests", + ldap_hex_md5 = "passlib.handlers.roundup", + ldap_hex_sha1 = "passlib.handlers.roundup", + ldap_salted_md5 = "passlib.handlers.ldap_digests", + ldap_salted_sha1 = "passlib.handlers.ldap_digests", + ldap_salted_sha256 = "passlib.handlers.ldap_digests", + ldap_salted_sha512 = "passlib.handlers.ldap_digests", + ldap_des_crypt = "passlib.handlers.ldap_digests", + ldap_bsdi_crypt = "passlib.handlers.ldap_digests", + ldap_md5_crypt = "passlib.handlers.ldap_digests", + ldap_bcrypt = "passlib.handlers.ldap_digests", + ldap_sha1_crypt = "passlib.handlers.ldap_digests", + ldap_sha256_crypt = "passlib.handlers.ldap_digests", + ldap_sha512_crypt = "passlib.handlers.ldap_digests", + ldap_pbkdf2_sha1 = "passlib.handlers.pbkdf2", + ldap_pbkdf2_sha256 = "passlib.handlers.pbkdf2", + ldap_pbkdf2_sha512 = "passlib.handlers.pbkdf2", + lmhash = "passlib.handlers.windows", + md5_crypt = "passlib.handlers.md5_crypt", + msdcc = "passlib.handlers.windows", + msdcc2 = "passlib.handlers.windows", + mssql2000 = "passlib.handlers.mssql", + mssql2005 = "passlib.handlers.mssql", + mysql323 = "passlib.handlers.mysql", + mysql41 = "passlib.handlers.mysql", + nthash = "passlib.handlers.windows", + oracle10 = "passlib.handlers.oracle", + oracle11 = "passlib.handlers.oracle", + pbkdf2_sha1 = "passlib.handlers.pbkdf2", + pbkdf2_sha256 = "passlib.handlers.pbkdf2", + pbkdf2_sha512 = "passlib.handlers.pbkdf2", + phpass = "passlib.handlers.phpass", + plaintext = "passlib.handlers.misc", + postgres_md5 = "passlib.handlers.postgres", + roundup_plaintext = "passlib.handlers.roundup", + scram = "passlib.handlers.scram", + scrypt = "passlib.handlers.scrypt", + sha1_crypt = "passlib.handlers.sha1_crypt", + sha256_crypt = "passlib.handlers.sha2_crypt", + sha512_crypt = "passlib.handlers.sha2_crypt", + sun_md5_crypt = "passlib.handlers.sun_md5_crypt", + unix_disabled = "passlib.handlers.misc", + unix_fallback = "passlib.handlers.misc", +) + +# master regexp for detecting valid handler names +_name_re = re.compile("^[a-z][a-z0-9_]+[a-z0-9]$") + +# names which aren't allowed for various reasons +# (mainly keyword conflicts in CryptContext) +_forbidden_names = frozenset(["onload", "policy", "context", "all", + "default", "none", "auto"]) + +#============================================================================= +# registry frontend functions +#============================================================================= +def _validate_handler_name(name): + """helper to validate handler name + + :raises ValueError: + * if empty name + * if name not lower case + * if name contains double underscores + * if name is reserved (e.g. ``context``, ``all``). + """ + if not name: + raise ValueError("handler name cannot be empty: %r" % (name,)) + if name.lower() != name: + raise ValueError("name must be lower-case: %r" % (name,)) + if not _name_re.match(name): + raise ValueError("invalid name (must be 3+ characters, " + " begin with a-z, and contain only underscore, a-z, " + "0-9): %r" % (name,)) + if '__' in name: + raise ValueError("name may not contain double-underscores: %r" % + (name,)) + if name in _forbidden_names: + raise ValueError("that name is not allowed: %r" % (name,)) + return True + +def register_crypt_handler_path(name, path): + """register location to lazy-load handler when requested. + + custom hashes may be registered via :func:`register_crypt_handler`, + or they may be registered by this function, + which will delay actually importing and loading the handler + until a call to :func:`get_crypt_handler` is made for the specified name. + + :arg name: name of handler + :arg path: module import path + + the specified module path should contain a password hash handler + called :samp:`{name}`, or the path may contain a colon, + specifying the module and module attribute to use. + for example, the following would cause ``get_handler("myhash")`` to look + for a class named ``myhash`` within the ``myapp.helpers`` module:: + + >>> from passlib.registry import registry_crypt_handler_path + >>> registry_crypt_handler_path("myhash", "myapp.helpers") + + ...while this form would cause ``get_handler("myhash")`` to look + for a class name ``MyHash`` within the ``myapp.helpers`` module:: + + >>> from passlib.registry import registry_crypt_handler_path + >>> registry_crypt_handler_path("myhash", "myapp.helpers:MyHash") + """ + # validate name + _validate_handler_name(name) + + # validate path + if path.startswith("."): + raise ValueError("path cannot start with '.'") + if ':' in path: + if path.count(':') > 1: + raise ValueError("path cannot have more than one ':'") + if path.find('.', path.index(':')) > -1: + raise ValueError("path cannot have '.' to right of ':'") + + # store location + _locations[name] = path + log.debug("registered path to %r handler: %r", name, path) + +def register_crypt_handler(handler, force=False, _attr=None): + """register password hash handler. + + this method immediately registers a handler with the internal passlib registry, + so that it will be returned by :func:`get_crypt_handler` when requested. + + :arg handler: the password hash handler to register + :param force: force override of existing handler (defaults to False) + :param _attr: + [internal kwd] if specified, ensures ``handler.name`` + matches this value, or raises :exc:`ValueError`. + + :raises TypeError: + if the specified object does not appear to be a valid handler. + + :raises ValueError: + if the specified object's name (or other required attributes) + contain invalid values. + + :raises KeyError: + if a (different) handler was already registered with + the same name, and ``force=True`` was not specified. + """ + # validate handler + if not is_crypt_handler(handler): + raise ExpectedTypeError(handler, "password hash handler", "handler") + if not handler: + raise AssertionError("``bool(handler)`` must be True") + + # validate name + name = handler.name + _validate_handler_name(name) + if _attr and _attr != name: + raise ValueError("handlers must be stored only under their own name (%r != %r)" % + (_attr, name)) + + # check for existing handler + other = _handlers.get(name) + if other: + if other is handler: + log.debug("same %r handler already registered: %r", name, handler) + return + elif force: + log.warning("overriding previously registered %r handler: %r", + name, other) + else: + raise KeyError("another %r handler has already been registered: %r" % + (name, other)) + + # register handler + _handlers[name] = handler + log.debug("registered %r handler: %r", name, handler) + +def get_crypt_handler(name, default=_UNSET): + """return handler for specified password hash scheme. + + this method looks up a handler for the specified scheme. + if the handler is not already loaded, + it checks if the location is known, and loads it first. + + :arg name: name of handler to return + :param default: optional default value to return if no handler with specified name is found. + + :raises KeyError: if no handler matching that name is found, and no default specified, a KeyError will be raised. + + :returns: handler attached to name, or default value (if specified). + """ + # catch invalid names before we check _handlers, + # since it's a module dict, and exposes things like __package__, etc. + if name.startswith("_"): + if default is _UNSET: + raise KeyError("invalid handler name: %r" % (name,)) + else: + return default + + # check if handler is already loaded + try: + return _handlers[name] + except KeyError: + pass + + # normalize name (and if changed, check dict again) + assert isinstance(name, unicode_or_str), "name must be string instance" + alt = name.replace("-","_").lower() + if alt != name: + warn("handler names should be lower-case, and use underscores instead " + "of hyphens: %r => %r" % (name, alt), PasslibWarning, + stacklevel=2) + name = alt + + # try to load using new name + try: + return _handlers[name] + except KeyError: + pass + + # check if lazy load mapping has been specified for this driver + path = _locations.get(name) + if path: + if ':' in path: + modname, modattr = path.split(":") + else: + modname, modattr = path, name + ##log.debug("loading %r handler from path: '%s:%s'", name, modname, modattr) + + # try to load the module - any import errors indicate runtime config, usually + # either missing package, or bad path provided to register_crypt_handler_path() + mod = __import__(modname, fromlist=[modattr], level=0) + + # first check if importing module triggered register_crypt_handler(), + # (this is discouraged due to its magical implicitness) + handler = _handlers.get(name) + if handler: + # XXX: issue deprecation warning here? + assert is_crypt_handler(handler), "unexpected object: name=%r object=%r" % (name, handler) + return handler + + # then get real handler & register it + handler = getattr(mod, modattr) + register_crypt_handler(handler, _attr=name) + return handler + + # fail! + if default is _UNSET: + raise KeyError("no crypt handler found for algorithm: %r" % (name,)) + else: + return default + +def list_crypt_handlers(loaded_only=False): + """return sorted list of all known crypt handler names. + + :param loaded_only: if ``True``, only returns names of handlers which have actually been loaded. + + :returns: list of names of all known handlers + """ + names = set(_handlers) + if not loaded_only: + names.update(_locations) + # strip private attrs out of namespace and sort. + # TODO: make _handlers a separate list, so we don't have module namespace mixed in. + return sorted(name for name in names if not name.startswith("_")) + +# NOTE: these two functions mainly exist just for the unittests... + +def _has_crypt_handler(name, loaded_only=False): + """check if handler name is known. + + this is only useful for two cases: + + * quickly checking if handler has already been loaded + * checking if handler exists, without actually loading it + + :arg name: name of handler + :param loaded_only: if ``True``, returns False if handler exists but hasn't been loaded + """ + return (name in _handlers) or (not loaded_only and name in _locations) + +def _unload_handler_name(name, locations=True): + """unloads a handler from the registry. + + .. warning:: + + this is an internal function, + used only by the unittests. + + if loaded handler is found with specified name, it's removed. + if path to lazy load handler is found, it's removed. + + missing names are a noop. + + :arg name: name of handler to unload + :param locations: if False, won't purge registered handler locations (default True) + """ + if name in _handlers: + del _handlers[name] + if locations and name in _locations: + del _locations[name] + +#============================================================================= +# inspection helpers +#============================================================================= + +#------------------------------------------------------------------ +# general +#------------------------------------------------------------------ + +# TODO: needs UTs +def _resolve(hasher, param="value"): + """ + internal helper to resolve argument to hasher object + """ + if is_crypt_handler(hasher): + return hasher + elif isinstance(hasher, unicode_or_str): + return get_crypt_handler(hasher) + else: + raise exc.ExpectedTypeError(hasher, unicode_or_str, param) + + +#: backend aliases +ANY = "any" +BUILTIN = "builtin" +OS_CRYPT = "os_crypt" + +# TODO: needs UTs +def has_backend(hasher, backend=ANY, safe=False): + """ + Test if specified backend is available for hasher. + + :param hasher: + Hasher name or object. + + :param backend: + Name of backend, or ``"any"`` if any backend will do. + For hashers without multiple backends, will pretend + they have a single backend named ``"builtin"``. + + :param safe: + By default, throws error if backend is unknown. + If ``safe=True``, will just return false value. + + :raises ValueError: + * if hasher name is unknown. + * if backend is unknown to hasher, and safe=False. + + :return: + True if backend available, False if not available, + and None if unknown + safe=True. + """ + hasher = _resolve(hasher) + + if backend == ANY: + if not hasattr(hasher, "get_backend"): + # single backend, assume it's loaded + return True + + # multiple backends, check at least one is loadable + try: + hasher.get_backend() + return True + except exc.MissingBackendError: + return False + + # test for specific backend + if hasattr(hasher, "has_backend"): + # multiple backends + if safe and backend not in hasher.backends: + return None + return hasher.has_backend(backend) + + # single builtin backend + if backend == BUILTIN: + return True + elif safe: + return None + else: + raise exc.UnknownBackendError(hasher, backend) + +#------------------------------------------------------------------ +# os crypt +#------------------------------------------------------------------ + +# TODO: move unix_crypt_schemes list to here. +# os_crypt_schemes -- alias for unix_crypt_schemes above + + +# TODO: needs UTs +@memoize_single_value +def get_supported_os_crypt_schemes(): + """ + return tuple of schemes which :func:`crypt.crypt` natively supports. + """ + if not os_crypt_present: + return () + cache = tuple(name for name in os_crypt_schemes + if get_crypt_handler(name).has_backend(OS_CRYPT)) + if not cache: # pragma: no cover -- sanity check + # no idea what OS this could happen on... + import platform + warn("crypt.crypt() function is present, but doesn't support any " + "formats known to passlib! (system=%r release=%r)" % + (platform.system(), platform.release()), + exc.PasslibRuntimeWarning) + return cache + + +# TODO: needs UTs +def has_os_crypt_support(hasher): + """ + check if hash is supported by native :func:`crypt.crypt` function. + if :func:`crypt.crypt` is not present, will always return False. + + :param hasher: + name or hasher object. + + :returns bool: + True if hash format is supported by OS, else False. + """ + return os_crypt_present and has_backend(hasher, OS_CRYPT, safe=True) + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/__init__.py b/ansible/lib/python3.11/site-packages/passlib/tests/__init__.py new file mode 100644 index 000000000..389da76e1 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/tests/__init__.py @@ -0,0 +1 @@ +"""passlib tests""" diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/__main__.py b/ansible/lib/python3.11/site-packages/passlib/tests/__main__.py new file mode 100644 index 000000000..242457684 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/tests/__main__.py @@ -0,0 +1,6 @@ +import os +from nose import run +run( + defaultTest=os.path.dirname(__file__), +) + diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..02345292eb1e9472c10f052fa494776eb1b9a972 GIT binary patch literal 216 zcmZ3^%ge<81oP6jq>BLQ#~=<2FhUuhK}x1Gq%cG=q%a0EXfjpt79GW zh_W)V6_u$IHc*M4?YsB%>^u3s)!G4v+tKsr)ako#)~Njz%YiFq5J5x~oO`8;B=npT zpnw$NlC7W2gLU;^v35rQ9^LuH6Wi>Gb3TJG`0(T7kW|bRsgq`eLs_W2k2VjtDynT2 zq}rnKc~y|fd|cI|*rYPSsv*QMRw%>+EWOX_gpYNC+$ORxnrB8wT&6Z2DC8<0@NANg zb$Zb4^|+05+|T62l^mhvnY7kl$X%-CLL@Ryh3ICJRpT(xQK9h2v|NDyj_o^IFhWQP t+tZ&_!rsf~E`d#1+1b6R&)V1RdE>PczIDO{qlA8fqc2a3O)^bq{0&UyWQqU) literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/_test_bad_register.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/_test_bad_register.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..78aaca68aab4981141d9e7639c9b6874121ea413 GIT binary patch literal 1047 zcma)4zi$&U6t;brfB+j1NKgPnWFx3-Ezm;Um3COMs}LaY1Q7i-AQf7DUMtfaYXE$Zk~iDP zP*sVl{R35}jdvU6o07bNZ-YajX4KnH05AJ$2h$-tli&!V5!iz}9 zA9n=h0y8@DxnPrCJo&Ejbz$1heUxFBO=5vPXG{Xh`fhPxHkHS(uFB;RpumRmd5{#1 zzyau@v`&&RoZz8D`X_p7U_5q0igDV&IE+XVD7%UAe&Pg0L|5|(Sg8ZfY|D#6YVpuX zSY*X4B9=4e{xG0c;18{MB592Gdb_(8_XWKdJMIf-M7b3^oGXB0$1R+Zly@kIQYMKq zRo6@@V|7ic!*yS5v=1Mv&YC6azlcLg97*C@YC3_yYx3DSeO6V;R6X;wrm|Y4GL&&; z9aR1UvmnyTh;*B24Pz3y^3hslq^uoNe=BX5?nL+j?-f%*XO&eS|Oz@Y!@Gji>%($Ny?;*P>k?I%l&r&Tl2;lLEdIMuszy*n$1BCwqSvI6}A$w~is literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/backports.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/backports.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ad7638d024eb6229fe8bb39614791d91ecb6ed96 GIT binary patch literal 1612 zcmZ`(&2Jk;6rb^~?X@>{65F*@WWfSyTuZ4fhtNW)A}WNS5|lv7A$wVEykmQl{cva2 z%7=txp&lZ{A-CQT;t)VwIKUskf8a=aSnVlNPuv!TL(~&**7i1mGPCn$-q*Z&?>DnQ z77BR;?YrJ@z1Ly&j5D*Ao-WQsjL=hLAQc(dNLkoUsVU6Yf+eUz3K3yPoH>#730VIE zW75=2Y?oED@hqtl%%YXE^J*TW9x~EDU{%gdcAHZRPVG%Zic5%AFd}&>!|p{Rb9K+J zalBVSEo>o2+(Y}q7TUw4urr@Tod<3q;TC{fOt?khN(om=p^uP}eS!=Lm5l?h?>fqA^R{wRkB>R^Rocxvjm|Wqc$%Sh{td8C5kV>j zlv4(dQ58vgCo?lQQYR^>2#iT%bCp4w(Jk+hJCaY zzIP;_$)%~qc=_E^`K~YDw_ zTNXf(OQ4Uz2;~Phe~TYd>Qc(DAeA`bS1Ob?;q~xKF)2`mE8@deVf9?PH-UYfz5y)% zTd|u!AvKHxv@%93k%aK_7~Q!1ouT3=?&E6xxCf>CNjj~z{c8JI2vBYm_W0cE_(6r$yoy$yg$1VtMr0 ztVcQUy12NAN=RCG^3_NTa*N--Hp&ErYkol)xU+@a6Pq-=A0hij{x= Wj|t;{+F!-D(eKs!Md6PE2Ko=(>ZJ+* literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_apache.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_apache.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..524f64d632b607578fbd900e31f6738672a69ddc GIT binary patch literal 45969 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 literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_apps.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_apps.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3c30bd508731fa8fe6bfc55f02c13b6c40bf21d6 GIT binary patch literal 7969 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 literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_pwd.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_pwd.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f1d0a74a9bb9feb6ca70c52362bf468d63c7029d GIT binary patch literal 10393 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 literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_totp.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_totp.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..35abf1788ec68288e04a828af5894dd0f4d1b7d8 GIT binary patch literal 71972 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 x3f!}2ME71H*a zRHPgtkpmo2Iia3{P>%2y@E15Jh-jsrIB_!y2ha5UR2C)Za0qOmHH2lx)nR z06R$>jtRsjd93uj0ka7S@=}QiTo%F}9~MZH(kLY=0T&EbC*U07TIcsnPEi8()i4ps z$RC?)X!PAhrJkpNyqC?WXo@S{jIn6Vt{ZSCAAg0iPUawvMTeoq& ziL*s|Gl`-krnhfhrFURkw(t@6nL93jxJAp&WEH~E6=1#r0C5DsL0!wkU3&)&07~0V zx8m8`s9W7Z_i?udFsX{XQOFl70JUvHczP=!U3aH>I})& zle?W^67i0V*hnOuR3u)9#WLuJyaT7Ola2vD@w@G{wT=uFUrm|&CG)xL6d`vEPz)_F z0$pgQV=)irH_t!Q{?L5}Hqti{ityJMqX*cOT<9f$QyXH0 zlAJqLcA>X6Ezy9<@OhPpSb!f{!{*rCFWQ{Ne)s45eRCea*H1R)iuIFWWPh5nBIx(37nc5iRsa407 zDbnO3!i!rA`k6Wi-^3I2{_%HOL)8mY+j_~`nwy@P$B@fuGP!X1AN~8^=jcpC{{y?$ B>u3M~ literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_utils_pbkdf2.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_utils_pbkdf2.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b08e8ee8a15bbad5e59fcc191a2f86589c117e73 GIT binary patch literal 14088 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 literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_win32.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_win32.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4b8a95bc887305c6fed1489c6fee2a36e043e911 GIT binary patch literal 2584 zcmb7G&2JM&6rc6(+8f6XQ6Y%Z1Xj?9Y}(jff5n!7k~V=-p-@36l0~&z@6N`i>yOT^ z6^Ifk2c)J_D{(<^p-L}lQF`nj(W8Sb5v_!T)KhOp;ZSkvoAF1pB&F?m{qvjm=HtD6 zzj@=IqR}vd_QT?@i)$f-ey5#Y6T8ChMGzh$6BUrjnE{(CaPZ69feK&XIjR$Ep&}MU z4h7I9WQzBY8N~c1kI)u;JFO5j1J{ufTK>qzj_b%)jmUM~aqMz2wp6aBlJeA)JYgvE zjN>$jUNx}1fQ?eMX4e+W*pZWoL~05k{|8-fUg11x0T%j1cQD9Y!|3kAegfUjZVZHP zz;}$mXH*E7T!CLl7DRDRDhMWj4;4gH01U39J6s_I9*UlH!!6r%@nxC%DmS1jTaBvW z$mOcvX@lFdtpGBOcXtio0rwEOd#rBHC$O!cP3Ud)H5+{ijQzF=(pJC6As6z~Yq#lM ztkI14SA&coh=ZT^g5||(jo{@8w}j<0zPEiwuGGwijpecGos2V*Fw_`3V)eU(GIe>V zmz)@L?kMP-PXYd1L(iq?_oYX|wlvm~#-2&1pGv2gcaUDmP6kuj2?sY zgr*A4PMGLRnq4XBPRSd*dS&+Wnd{eYUcEN!$wt1IQ_XxTZ)PnsmoUt9B8Rg@HIbQ4 ztGR628=RlJ`SNF|#LlYQ6F!~JK!{-?DqzNaG1DrUpI7H5lNI9sMQNP*dW;T1Qt?| zV5ecb4o#Fy8#%;gQ-*cJUOv(%)gJA8FwA)Xu!eS|51anyMfCXNndXTL+oLnB(V1rC zsX7Rb9WP6?7h_#qJrlrZEJ1wuKLXHF*DGh2XgRyg%GqUB&MtE={8Ry{mx6OeRY6|dGr@9L}nlW literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/tox_support.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/tox_support.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..90322983e9991c07b8d5c2ab45001daf29ba0753 GIT binary patch literal 3869 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 BtcU;r literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/utils.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/utils.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5e091209d6fb7fef2acc28bc14de441893202e37 GIT binary patch literal 159461 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 literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/_test_bad_register.py b/ansible/lib/python3.11/site-packages/passlib/tests/_test_bad_register.py new file mode 100644 index 000000000..f0683fcc3 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/tests/_test_bad_register.py @@ -0,0 +1,15 @@ +"""helper for method in test_registry.py""" +from passlib.registry import register_crypt_handler +import passlib.utils.handlers as uh + +class dummy_bad(uh.StaticHandler): + name = "dummy_bad" + +class alt_dummy_bad(uh.StaticHandler): + name = "dummy_bad" + +# NOTE: if passlib.tests is being run from symlink (e.g. via gaeunit), +# this module may be imported a second time as test._test_bad_registry. +# we don't want it to do anything in that case. +if __name__.startswith("passlib.tests"): + register_crypt_handler(alt_dummy_bad) diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/backports.py b/ansible/lib/python3.11/site-packages/passlib/tests/backports.py new file mode 100644 index 000000000..5058cec6c --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/tests/backports.py @@ -0,0 +1,67 @@ +"""backports of needed unittest2 features""" +#============================================================================= +# imports +#============================================================================= +from __future__ import with_statement +# core +import logging; log = logging.getLogger(__name__) +import re +import sys +##from warnings import warn +# site +# pkg +from passlib.utils.compat import PY26 +# local +__all__ = [ + "TestCase", + "unittest", + # TODO: deprecate these exports in favor of "unittest.XXX" + "skip", "skipIf", "skipUnless", +] + +#============================================================================= +# import latest unittest module available +#============================================================================= +try: + import unittest2 as unittest +except ImportError: + if PY26: + raise ImportError("Passlib's tests require 'unittest2' under Python 2.6 (as of Passlib 1.7)") + # python 2.7 and python 3.2 both have unittest2 features (at least, the ones we use) + import unittest + +#============================================================================= +# unittest aliases +#============================================================================= +skip = unittest.skip +skipIf = unittest.skipIf +skipUnless = unittest.skipUnless +SkipTest = unittest.SkipTest + +#============================================================================= +# custom test harness +#============================================================================= +class TestCase(unittest.TestCase): + """backports a number of unittest2 features in TestCase""" + + #=================================================================== + # backport some unittest2 names + #=================================================================== + + #--------------------------------------------------------------- + # backport assertRegex() alias from 3.2 to 2.7 + # was present in 2.7 under an alternate name + #--------------------------------------------------------------- + if not hasattr(unittest.TestCase, "assertRegex"): + assertRegex = unittest.TestCase.assertRegexpMatches + + if not hasattr(unittest.TestCase, "assertRaisesRegex"): + assertRaisesRegex = unittest.TestCase.assertRaisesRegexp + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/sample1.cfg b/ansible/lib/python3.11/site-packages/passlib/tests/sample1.cfg new file mode 100644 index 000000000..56e3ae8e7 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/tests/sample1.cfg @@ -0,0 +1,9 @@ +[passlib] +schemes = des_crypt, md5_crypt, bsdi_crypt, sha512_crypt +default = md5_crypt +all__vary_rounds = 0.1 +bsdi_crypt__default_rounds = 25001 +bsdi_crypt__max_rounds = 30001 +sha512_crypt__max_rounds = 50000 +sha512_crypt__min_rounds = 40000 + diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/sample1b.cfg b/ansible/lib/python3.11/site-packages/passlib/tests/sample1b.cfg new file mode 100644 index 000000000..542a6036b --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/tests/sample1b.cfg @@ -0,0 +1,9 @@ +[passlib] +schemes = des_crypt, md5_crypt, bsdi_crypt, sha512_crypt +default = md5_crypt +all__vary_rounds = 0.1 +bsdi_crypt__default_rounds = 25001 +bsdi_crypt__max_rounds = 30001 +sha512_crypt__max_rounds = 50000 +sha512_crypt__min_rounds = 40000 + diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/sample1c.cfg b/ansible/lib/python3.11/site-packages/passlib/tests/sample1c.cfg new file mode 100644 index 0000000000000000000000000000000000000000..a5033eb90ead55436ce1e5727a33fc6fa9c0842a GIT binary patch literal 490 zcmaKp-3o$05QWe6K1B}@VOEztNrY4y37U}>_43s>s}b26gUexN&(FBe$4aH{I;m2j zTs!buPBrMDj9CUAX~~y*oG>|iMx!y^lKw*d?iN;xUcX0G^*yr-IhxM# zSKD!;pc3i|wj>E>1@DN)J8Pr~9!{Yg#E=s)w^mOZqy$") + + #=================================================================== + # modifiers + #=================================================================== + def test_10_load(self): + """test load() / load_path() method""" + # NOTE: load() is the workhorse that handles all policy parsing, + # compilation, and validation. most of its features are tested + # elsewhere, since all the constructors and modifiers are just + # wrappers for it. + + # source_type 'auto' + ctx = CryptContext() + + # detect dict + ctx.load(self.sample_1_dict) + self.assertEqual(ctx.to_dict(), self.sample_1_dict) + + # detect unicode string + ctx.load(self.sample_1_unicode) + self.assertEqual(ctx.to_dict(), self.sample_1_dict) + + # detect bytes string + ctx.load(self.sample_1_unicode.encode("utf-8")) + self.assertEqual(ctx.to_dict(), self.sample_1_dict) + + # anything else - TypeError + self.assertRaises(TypeError, ctx.load, None) + + # NOTE: load_path() tested by from_path() + # NOTE: additional string tests done by from_string() + + # update flag - tested by update() method tests + # encoding keyword - tested by from_string() & from_path() + # section keyword - tested by from_string() & from_path() + + # test load empty + ctx = CryptContext(**self.sample_1_dict) + ctx.load({}, update=True) + self.assertEqual(ctx.to_dict(), self.sample_1_dict) + + # multiple loads should clear the state + ctx = CryptContext() + ctx.load(self.sample_1_dict) + ctx.load(self.sample_2_dict) + self.assertEqual(ctx.to_dict(), self.sample_2_dict) + + def test_11_load_rollback(self): + """test load() errors restore old state""" + # create initial context + cc = CryptContext(["des_crypt", "sha256_crypt"], + sha256_crypt__default_rounds=5000, + all__vary_rounds=0.1, + ) + result = cc.to_string() + + # do an update operation that should fail during parsing + # XXX: not sure what the right error type is here. + self.assertRaises(TypeError, cc.update, too__many__key__parts=True) + self.assertEqual(cc.to_string(), result) + + # do an update operation that should fail during extraction + # FIXME: this isn't failing even in broken case, need to figure out + # way to ensure some keys come after this one. + self.assertRaises(KeyError, cc.update, fake_context_option=True) + self.assertEqual(cc.to_string(), result) + + # do an update operation that should fail during compilation + self.assertRaises(ValueError, cc.update, sha256_crypt__min_rounds=10000) + self.assertEqual(cc.to_string(), result) + + def test_12_update(self): + """test update() method""" + + # empty overlay + ctx = CryptContext(**self.sample_1_dict) + ctx.update() + self.assertEqual(ctx.to_dict(), self.sample_1_dict) + + # test basic overlay + ctx = CryptContext(**self.sample_1_dict) + ctx.update(**self.sample_2_dict) + self.assertEqual(ctx.to_dict(), self.sample_12_dict) + + # ... and again + ctx.update(**self.sample_3_dict) + self.assertEqual(ctx.to_dict(), self.sample_123_dict) + + # overlay w/ dict arg + ctx = CryptContext(**self.sample_1_dict) + ctx.update(self.sample_2_dict) + self.assertEqual(ctx.to_dict(), self.sample_12_dict) + + # overlay w/ string + ctx = CryptContext(**self.sample_1_dict) + ctx.update(self.sample_2_unicode) + self.assertEqual(ctx.to_dict(), self.sample_12_dict) + + # too many args + self.assertRaises(TypeError, ctx.update, {}, {}) + self.assertRaises(TypeError, ctx.update, {}, schemes=['des_crypt']) + + # wrong arg type + self.assertRaises(TypeError, ctx.update, None) + + #=================================================================== + # option parsing + #=================================================================== + def test_20_options(self): + """test basic option parsing""" + def parse(**kwds): + return CryptContext(**kwds).to_dict() + + # + # common option parsing tests + # + + # test keys with blank fields are rejected + # blank option + self.assertRaises(TypeError, CryptContext, __=0.1) + self.assertRaises(TypeError, CryptContext, default__scheme__='x') + + # blank scheme + self.assertRaises(TypeError, CryptContext, __option='x') + self.assertRaises(TypeError, CryptContext, default____option='x') + + # blank category + self.assertRaises(TypeError, CryptContext, __scheme__option='x') + + # test keys with too many field are rejected + self.assertRaises(TypeError, CryptContext, + category__scheme__option__invalid = 30000) + + # keys with mixed separators should be handled correctly. + # (testing actual data, not to_dict(), since re-render hid original bug) + self.assertRaises(KeyError, parse, + **{"admin.context__schemes":"md5_crypt"}) + ctx = CryptContext(**{"schemes":"md5_crypt,des_crypt", + "admin.context__default":"des_crypt"}) + self.assertEqual(ctx.default_scheme("admin"), "des_crypt") + + # + # context option -specific tests + # + + # test context option key parsing + result = dict(default="md5_crypt") + self.assertEqual(parse(default="md5_crypt"), result) + self.assertEqual(parse(context__default="md5_crypt"), result) + self.assertEqual(parse(default__context__default="md5_crypt"), result) + self.assertEqual(parse(**{"context.default":"md5_crypt"}), result) + self.assertEqual(parse(**{"default.context.default":"md5_crypt"}), result) + + # test context option key parsing w/ category + result = dict(admin__context__default="md5_crypt") + self.assertEqual(parse(admin__context__default="md5_crypt"), result) + self.assertEqual(parse(**{"admin.context.default":"md5_crypt"}), result) + + # + # hash option -specific tests + # + + # test hash option key parsing + result = dict(all__vary_rounds=0.1) + self.assertEqual(parse(all__vary_rounds=0.1), result) + self.assertEqual(parse(default__all__vary_rounds=0.1), result) + self.assertEqual(parse(**{"all.vary_rounds":0.1}), result) + self.assertEqual(parse(**{"default.all.vary_rounds":0.1}), result) + + # test hash option key parsing w/ category + result = dict(admin__all__vary_rounds=0.1) + self.assertEqual(parse(admin__all__vary_rounds=0.1), result) + self.assertEqual(parse(**{"admin.all.vary_rounds":0.1}), result) + + # settings not allowed if not in hash.setting_kwds + ctx = CryptContext(["phpass", "md5_crypt"], phpass__ident="P") + self.assertRaises(KeyError, ctx.copy, md5_crypt__ident="P") + + # hash options 'salt' and 'rounds' not allowed + self.assertRaises(KeyError, CryptContext, schemes=["des_crypt"], + des_crypt__salt="xx") + self.assertRaises(KeyError, CryptContext, schemes=["des_crypt"], + all__salt="xx") + + def test_21_schemes(self): + """test 'schemes' context option parsing""" + + # schemes can be empty + cc = CryptContext(schemes=None) + self.assertEqual(cc.schemes(), ()) + + # schemes can be list of names + cc = CryptContext(schemes=["des_crypt", "md5_crypt"]) + self.assertEqual(cc.schemes(), ("des_crypt", "md5_crypt")) + + # schemes can be comma-sep string + cc = CryptContext(schemes=" des_crypt, md5_crypt, ") + self.assertEqual(cc.schemes(), ("des_crypt", "md5_crypt")) + + # schemes can be list of handlers + cc = CryptContext(schemes=[hash.des_crypt, hash.md5_crypt]) + self.assertEqual(cc.schemes(), ("des_crypt", "md5_crypt")) + + # scheme must be name or handler + self.assertRaises(TypeError, CryptContext, schemes=[uh.StaticHandler]) + + # handlers must have a name + class nameless(uh.StaticHandler): + name = None + self.assertRaises(ValueError, CryptContext, schemes=[nameless]) + + # names must be unique + class dummy_1(uh.StaticHandler): + name = 'dummy_1' + self.assertRaises(KeyError, CryptContext, schemes=[dummy_1, dummy_1]) + + # schemes not allowed per-category + self.assertRaises(KeyError, CryptContext, + admin__context__schemes=["md5_crypt"]) + + def test_22_deprecated(self): + """test 'deprecated' context option parsing""" + def getdep(ctx, category=None): + return [name for name in ctx.schemes() + if ctx.handler(name, category).deprecated] + + # no schemes - all deprecated values allowed + cc = CryptContext(deprecated=["md5_crypt"]) + cc.update(schemes=["md5_crypt", "des_crypt"]) + self.assertEqual(getdep(cc),["md5_crypt"]) + + # deprecated values allowed if subset of schemes + cc = CryptContext(deprecated=["md5_crypt"], schemes=["md5_crypt", "des_crypt"]) + self.assertEqual(getdep(cc), ["md5_crypt"]) + + # can be handler + # XXX: allow handlers in deprecated list? not for now. + self.assertRaises(TypeError, CryptContext, deprecated=[hash.md5_crypt], + schemes=["md5_crypt", "des_crypt"]) +## cc = CryptContext(deprecated=[hash.md5_crypt], schemes=["md5_crypt", "des_crypt"]) +## self.assertEqual(getdep(cc), ["md5_crypt"]) + + # comma sep list + cc = CryptContext(deprecated="md5_crypt,des_crypt", schemes=["md5_crypt", "des_crypt", "sha256_crypt"]) + self.assertEqual(getdep(cc), ["md5_crypt", "des_crypt"]) + + # values outside of schemes not allowed + self.assertRaises(KeyError, CryptContext, schemes=['des_crypt'], + deprecated=['md5_crypt']) + + # deprecating ALL schemes should cause ValueError + self.assertRaises(ValueError, CryptContext, + schemes=['des_crypt'], + deprecated=['des_crypt']) + self.assertRaises(ValueError, CryptContext, + schemes=['des_crypt', 'md5_crypt'], + admin__context__deprecated=['des_crypt', 'md5_crypt']) + + # deprecating explicit default scheme should cause ValueError + + # ... default listed as deprecated + self.assertRaises(ValueError, CryptContext, + schemes=['des_crypt', 'md5_crypt'], + default="md5_crypt", + deprecated="md5_crypt") + + # ... global default deprecated per-category + self.assertRaises(ValueError, CryptContext, + schemes=['des_crypt', 'md5_crypt'], + default="md5_crypt", + admin__context__deprecated="md5_crypt") + + # ... category default deprecated globally + self.assertRaises(ValueError, CryptContext, + schemes=['des_crypt', 'md5_crypt'], + admin__context__default="md5_crypt", + deprecated="md5_crypt") + + # ... category default deprecated in category + self.assertRaises(ValueError, CryptContext, + schemes=['des_crypt', 'md5_crypt'], + admin__context__default="md5_crypt", + admin__context__deprecated="md5_crypt") + + # category deplist should shadow default deplist + CryptContext( + schemes=['des_crypt', 'md5_crypt'], + deprecated="md5_crypt", + admin__context__default="md5_crypt", + admin__context__deprecated=[]) + + # wrong type + self.assertRaises(TypeError, CryptContext, deprecated=123) + + # deprecated per-category + cc = CryptContext(deprecated=["md5_crypt"], + schemes=["md5_crypt", "des_crypt"], + admin__context__deprecated=["des_crypt"], + ) + self.assertEqual(getdep(cc), ["md5_crypt"]) + self.assertEqual(getdep(cc, "user"), ["md5_crypt"]) + self.assertEqual(getdep(cc, "admin"), ["des_crypt"]) + + # blank per-category deprecated list, shadowing default list + cc = CryptContext(deprecated=["md5_crypt"], + schemes=["md5_crypt", "des_crypt"], + admin__context__deprecated=[], + ) + self.assertEqual(getdep(cc), ["md5_crypt"]) + self.assertEqual(getdep(cc, "user"), ["md5_crypt"]) + self.assertEqual(getdep(cc, "admin"), []) + + def test_23_default(self): + """test 'default' context option parsing""" + + # anything allowed if no schemes + self.assertEqual(CryptContext(default="md5_crypt").to_dict(), + dict(default="md5_crypt")) + + # default allowed if in scheme list + ctx = CryptContext(default="md5_crypt", schemes=["des_crypt", "md5_crypt"]) + self.assertEqual(ctx.default_scheme(), "md5_crypt") + + # default can be handler + # XXX: sure we want to allow this ? maybe deprecate in future. + ctx = CryptContext(default=hash.md5_crypt, schemes=["des_crypt", "md5_crypt"]) + self.assertEqual(ctx.default_scheme(), "md5_crypt") + + # implicit default should be first non-deprecated scheme + ctx = CryptContext(schemes=["des_crypt", "md5_crypt"]) + self.assertEqual(ctx.default_scheme(), "des_crypt") + ctx.update(deprecated="des_crypt") + self.assertEqual(ctx.default_scheme(), "md5_crypt") + + # error if not in scheme list + self.assertRaises(KeyError, CryptContext, schemes=['des_crypt'], + default='md5_crypt') + + # wrong type + self.assertRaises(TypeError, CryptContext, default=1) + + # per-category + ctx = CryptContext(default="des_crypt", + schemes=["des_crypt", "md5_crypt"], + admin__context__default="md5_crypt") + self.assertEqual(ctx.default_scheme(), "des_crypt") + self.assertEqual(ctx.default_scheme("user"), "des_crypt") + self.assertEqual(ctx.default_scheme("admin"), "md5_crypt") + + def test_24_vary_rounds(self): + """test 'vary_rounds' hash option parsing""" + def parse(v): + return CryptContext(all__vary_rounds=v).to_dict()['all__vary_rounds'] + + # floats should be preserved + self.assertEqual(parse(0.1), 0.1) + self.assertEqual(parse('0.1'), 0.1) + + # 'xx%' should be converted to float + self.assertEqual(parse('10%'), 0.1) + + # ints should be preserved + self.assertEqual(parse(1000), 1000) + self.assertEqual(parse('1000'), 1000) + + #=================================================================== + # inspection & serialization + #=================================================================== + + def assertHandlerDerivedFrom(self, handler, base, msg=None): + self.assertTrue(handler_derived_from(handler, base), msg=msg) + + def test_30_schemes(self): + """test schemes() method""" + # NOTE: also checked under test_21 + + # test empty + ctx = CryptContext() + self.assertEqual(ctx.schemes(), ()) + self.assertEqual(ctx.schemes(resolve=True), ()) + + # test sample 1 + ctx = CryptContext(**self.sample_1_dict) + self.assertEqual(ctx.schemes(), tuple(self.sample_1_schemes)) + self.assertEqual(ctx.schemes(resolve=True, unconfigured=True), tuple(self.sample_1_handlers)) + for result, correct in zip(ctx.schemes(resolve=True), self.sample_1_handlers): + self.assertTrue(handler_derived_from(result, correct)) + + # test sample 2 + ctx = CryptContext(**self.sample_2_dict) + self.assertEqual(ctx.schemes(), ()) + + def test_31_default_scheme(self): + """test default_scheme() method""" + # NOTE: also checked under test_23 + + # test empty + ctx = CryptContext() + self.assertRaises(KeyError, ctx.default_scheme) + + # test sample 1 + ctx = CryptContext(**self.sample_1_dict) + self.assertEqual(ctx.default_scheme(), "md5_crypt") + self.assertEqual(ctx.default_scheme(resolve=True, unconfigured=True), hash.md5_crypt) + self.assertHandlerDerivedFrom(ctx.default_scheme(resolve=True), hash.md5_crypt) + + # test sample 2 + ctx = CryptContext(**self.sample_2_dict) + self.assertRaises(KeyError, ctx.default_scheme) + + # test defaults to first in scheme + ctx = CryptContext(schemes=self.sample_1_schemes) + self.assertEqual(ctx.default_scheme(), "des_crypt") + + # categories tested under test_23 + + def test_32_handler(self): + """test handler() method""" + + # default for empty + ctx = CryptContext() + self.assertRaises(KeyError, ctx.handler) + self.assertRaises(KeyError, ctx.handler, "md5_crypt") + + # default for sample 1 + ctx = CryptContext(**self.sample_1_dict) + self.assertEqual(ctx.handler(unconfigured=True), hash.md5_crypt) + self.assertHandlerDerivedFrom(ctx.handler(), hash.md5_crypt) + + # by name + self.assertEqual(ctx.handler("des_crypt", unconfigured=True), hash.des_crypt) + self.assertHandlerDerivedFrom(ctx.handler("des_crypt"), hash.des_crypt) + + # name not in schemes + self.assertRaises(KeyError, ctx.handler, "mysql323") + + # check handler() honors category default + ctx = CryptContext("sha256_crypt,md5_crypt", admin__context__default="md5_crypt") + self.assertEqual(ctx.handler(unconfigured=True), hash.sha256_crypt) + self.assertHandlerDerivedFrom(ctx.handler(), hash.sha256_crypt) + + self.assertEqual(ctx.handler(category="staff", unconfigured=True), hash.sha256_crypt) + self.assertHandlerDerivedFrom(ctx.handler(category="staff"), hash.sha256_crypt) + + self.assertEqual(ctx.handler(category="admin", unconfigured=True), hash.md5_crypt) + self.assertHandlerDerivedFrom(ctx.handler(category="staff"), hash.sha256_crypt) + + # test unicode category strings are accepted under py2 + if PY2: + self.assertEqual(ctx.handler(category=u("staff"), unconfigured=True), hash.sha256_crypt) + self.assertEqual(ctx.handler(category=u("admin"), unconfigured=True), hash.md5_crypt) + + def test_33_options(self): + """test internal _get_record_options() method""" + + def options(ctx, scheme, category=None): + return ctx._config._get_record_options_with_flag(scheme, category)[0] + + # this checks that (3 schemes, 3 categories) inherit options correctly. + # the 'user' category is not present in the options. + cc4 = CryptContext( + truncate_error=True, + schemes = [ "sha512_crypt", "des_crypt", "bsdi_crypt"], + deprecated = ["sha512_crypt", "des_crypt"], + all__vary_rounds = 0.1, + bsdi_crypt__vary_rounds=0.2, + sha512_crypt__max_rounds = 20000, + admin__context__deprecated = [ "des_crypt", "bsdi_crypt" ], + admin__all__vary_rounds = 0.05, + admin__bsdi_crypt__vary_rounds=0.3, + admin__sha512_crypt__max_rounds = 40000, + ) + self.assertEqual(cc4._config.categories, ("admin",)) + + # + # sha512_crypt + # NOTE: 'truncate_error' shouldn't be passed along... + # + self.assertEqual(options(cc4, "sha512_crypt"), dict( + deprecated=True, + vary_rounds=0.1, # inherited from all__ + max_rounds=20000, + )) + + self.assertEqual(options(cc4, "sha512_crypt", "user"), dict( + deprecated=True, # unconfigured category inherits from default + vary_rounds=0.1, + max_rounds=20000, + )) + + self.assertEqual(options(cc4, "sha512_crypt", "admin"), dict( + # NOT deprecated - context option overridden per-category + vary_rounds=0.05, # global overridden per-cateogry + max_rounds=40000, # overridden per-category + )) + + # + # des_crypt + # NOTE: vary_rounds shouldn't be passed along... + # + self.assertEqual(options(cc4, "des_crypt"), dict( + deprecated=True, + truncate_error=True, + )) + + self.assertEqual(options(cc4, "des_crypt", "user"), dict( + deprecated=True, # unconfigured category inherits from default + truncate_error=True, + )) + + self.assertEqual(options(cc4, "des_crypt", "admin"), dict( + deprecated=True, # unchanged though overidden + truncate_error=True, + )) + + # + # bsdi_crypt + # + self.assertEqual(options(cc4, "bsdi_crypt"), dict( + vary_rounds=0.2, # overridden from all__vary_rounds + )) + + self.assertEqual(options(cc4, "bsdi_crypt", "user"), dict( + vary_rounds=0.2, # unconfigured category inherits from default + )) + + self.assertEqual(options(cc4, "bsdi_crypt", "admin"), dict( + vary_rounds=0.3, + deprecated=True, # deprecation set per-category + )) + + def test_34_to_dict(self): + """test to_dict() method""" + # NOTE: this is tested all throughout this test case. + ctx = CryptContext(**self.sample_1_dict) + self.assertEqual(ctx.to_dict(), self.sample_1_dict) + self.assertEqual(ctx.to_dict(resolve=True), self.sample_1_resolved_dict) + + def test_35_to_string(self): + """test to_string() method""" + + # create ctx and serialize + ctx = CryptContext(**self.sample_1_dict) + dump = ctx.to_string() + + # check ctx->string returns canonical format. + # NOTE: ConfigParser for PY26 doesn't use OrderedDict, + # making to_string()'s ordering unpredictable... + # so we skip this test under PY26. + if not PY26: + self.assertEqual(dump, self.sample_1_unicode) + + # check ctx->string->ctx->dict returns original + ctx2 = CryptContext.from_string(dump) + self.assertEqual(ctx2.to_dict(), self.sample_1_dict) + + # test section kwd is honored + other = ctx.to_string(section="password-security") + self.assertEqual(other, dump.replace("[passlib]","[password-security]")) + + # test unmanaged handler warning + from passlib.tests.test_utils_handlers import UnsaltedHash + ctx3 = CryptContext([UnsaltedHash, "md5_crypt"]) + dump = ctx3.to_string() + self.assertRegex(dump, r"# NOTE: the 'unsalted_test_hash' handler\(s\)" + r" are not registered with Passlib") + + #=================================================================== + # password hash api + #=================================================================== + nonstring_vectors = [ + (None, {}), + (None, {"scheme": "des_crypt"}), + (1, {}), + ((), {}), + ] + + def test_40_basic(self): + """test basic hash/identify/verify functionality""" + handlers = [hash.md5_crypt, hash.des_crypt, hash.bsdi_crypt] + cc = CryptContext(handlers, bsdi_crypt__default_rounds=5) + + # run through handlers + for crypt in handlers: + h = cc.hash("test", scheme=crypt.name) + self.assertEqual(cc.identify(h), crypt.name) + self.assertEqual(cc.identify(h, resolve=True, unconfigured=True), crypt) + self.assertHandlerDerivedFrom(cc.identify(h, resolve=True), crypt) + self.assertTrue(cc.verify('test', h)) + self.assertFalse(cc.verify('notest', h)) + + # test default + h = cc.hash("test") + self.assertEqual(cc.identify(h), "md5_crypt") + + # test genhash + h = cc.genhash('secret', cc.genconfig()) + self.assertEqual(cc.identify(h), 'md5_crypt') + + h = cc.genhash('secret', cc.genconfig(), scheme='md5_crypt') + self.assertEqual(cc.identify(h), 'md5_crypt') + + self.assertRaises(ValueError, cc.genhash, 'secret', cc.genconfig(), scheme="des_crypt") + + def test_41_genconfig(self): + """test genconfig() method""" + cc = CryptContext(schemes=["md5_crypt", "phpass"], + phpass__ident="H", + phpass__default_rounds=7, + admin__phpass__ident="P", + ) + + # uses default scheme + self.assertTrue(cc.genconfig().startswith("$1$")) + + # override scheme + self.assertTrue(cc.genconfig(scheme="phpass").startswith("$H$5")) + + # category override + self.assertTrue(cc.genconfig(scheme="phpass", category="admin").startswith("$P$5")) + self.assertTrue(cc.genconfig(scheme="phpass", category="staff").startswith("$H$5")) + + # override scheme & custom settings + self.assertEqual( + cc.genconfig(scheme="phpass", salt='.'*8, rounds=8, ident='P'), + '$P$6........22zGEuacuPOqEpYPDeR0R/', # NOTE: config string generated w/ rounds=1 + ) + + #-------------------------------------------------------------- + # border cases + #-------------------------------------------------------------- + + # test unicode category strings are accepted under py2 + # this tests basic _get_record() used by hash/genhash/verify. + # we have to omit scheme=xxx so codepath is tested fully + if PY2: + c2 = cc.copy(default="phpass") + self.assertTrue(c2.genconfig(category=u("admin")).startswith("$P$5")) + self.assertTrue(c2.genconfig(category=u("staff")).startswith("$H$5")) + + # throws error without schemes + self.assertRaises(KeyError, CryptContext().genconfig) + self.assertRaises(KeyError, CryptContext().genconfig, scheme='md5_crypt') + + # bad scheme values + self.assertRaises(KeyError, cc.genconfig, scheme="fake") # XXX: should this be ValueError? + self.assertRaises(TypeError, cc.genconfig, scheme=1, category='staff') + self.assertRaises(TypeError, cc.genconfig, scheme=1) + + # bad category values + self.assertRaises(TypeError, cc.genconfig, category=1) + + + def test_42_genhash(self): + """test genhash() method""" + + #-------------------------------------------------------------- + # border cases + #-------------------------------------------------------------- + + # rejects non-string secrets + cc = CryptContext(["des_crypt"]) + hash = cc.hash('stub') + for secret, kwds in self.nonstring_vectors: + self.assertRaises(TypeError, cc.genhash, secret, hash, **kwds) + + # rejects non-string config strings + cc = CryptContext(["des_crypt"]) + for config, kwds in self.nonstring_vectors: + if hash is None: + # NOTE: as of 1.7, genhash is just wrapper for hash(), + # and handles genhash(secret, None) fine. + continue + self.assertRaises(TypeError, cc.genhash, 'secret', config, **kwds) + + # rejects config=None, even if default scheme lacks config string + cc = CryptContext(["mysql323"]) + self.assertRaises(TypeError, cc.genhash, "stub", None) + + # throws error without schemes + self.assertRaises(KeyError, CryptContext().genhash, 'secret', 'hash') + + # bad scheme values + self.assertRaises(KeyError, cc.genhash, 'secret', hash, scheme="fake") # XXX: should this be ValueError? + self.assertRaises(TypeError, cc.genhash, 'secret', hash, scheme=1) + + # bad category values + self.assertRaises(TypeError, cc.genconfig, 'secret', hash, category=1) + + def test_43_hash(self,): + """test hash() method""" + # XXX: what more can we test here that isn't deprecated + # or handled under another test (e.g. context kwds?) + + # respects rounds + cc = CryptContext(**self.sample_4_dict) + hash = cc.hash("password") + self.assertTrue(hash.startswith("$5$rounds=3000$")) + self.assertTrue(cc.verify("password", hash)) + self.assertFalse(cc.verify("passwordx", hash)) + + # make default > max throws error if attempted + # XXX: move this to copy() test? + self.assertRaises(ValueError, cc.copy, + sha256_crypt__default_rounds=4000) + + # rejects non-string secrets + cc = CryptContext(["des_crypt"]) + for secret, kwds in self.nonstring_vectors: + self.assertRaises(TypeError, cc.hash, secret, **kwds) + + # throws error without schemes + self.assertRaises(KeyError, CryptContext().hash, 'secret') + + # bad category values + self.assertRaises(TypeError, cc.hash, 'secret', category=1) + + def test_43_hash_legacy(self, use_16_legacy=False): + """test hash() method -- legacy 'scheme' and settings keywords""" + cc = CryptContext(**self.sample_4_dict) + + # TODO: should migrate these tests elsewhere, or remove them. + # can be replaced with following equivalent: + # + # def wrapper(secret, scheme=None, category=None, **kwds): + # handler = cc.handler(scheme, category) + # if kwds: + # handler = handler.using(**kwds) + # return handler.hash(secret) + # + # need to make sure bits being tested here are tested + # under the tests for the equivalent methods called above, + # and then discard the rest of these under 2.0. + + # hash specific settings + with self.assertWarningList(["passing settings to.*is deprecated"]): + self.assertEqual( + cc.hash("password", scheme="phpass", salt='.'*8), + '$H$5........De04R5Egz0aq8Tf.1eVhY/', + ) + with self.assertWarningList(["passing settings to.*is deprecated"]): + self.assertEqual( + cc.hash("password", scheme="phpass", salt='.'*8, ident="P"), + '$P$5........De04R5Egz0aq8Tf.1eVhY/', + ) + + # NOTE: more thorough job of rounds limits done below. + + # min rounds + with self.assertWarningList(["passing settings to.*is deprecated"]): + self.assertEqual( + cc.hash("password", rounds=1999, salt="nacl"), + '$5$rounds=1999$nacl$nmfwJIxqj0csloAAvSER0B8LU0ERCAbhmMug4Twl609', + ) + + with self.assertWarningList(["passing settings to.*is deprecated"]): + self.assertEqual( + cc.hash("password", rounds=2001, salt="nacl"), + '$5$rounds=2001$nacl$8PdeoPL4aXQnJ0woHhqgIw/efyfCKC2WHneOpnvF.31' + ) + # NOTE: max rounds, etc tested in genconfig() + + # bad scheme values + self.assertRaises(KeyError, cc.hash, 'secret', scheme="fake") # XXX: should this be ValueError? + self.assertRaises(TypeError, cc.hash, 'secret', scheme=1) + + def test_44_identify(self): + """test identify() border cases""" + handlers = ["md5_crypt", "des_crypt", "bsdi_crypt"] + cc = CryptContext(handlers, bsdi_crypt__default_rounds=5) + + # check unknown hash + self.assertEqual(cc.identify('$9$232323123$1287319827'), None) + self.assertRaises(ValueError, cc.identify, '$9$232323123$1287319827', required=True) + + #-------------------------------------------------------------- + # border cases + #-------------------------------------------------------------- + + # rejects non-string hashes + cc = CryptContext(["des_crypt"]) + for hash, kwds in self.nonstring_vectors: + self.assertRaises(TypeError, cc.identify, hash, **kwds) + + # throws error without schemes + cc = CryptContext() + self.assertIs(cc.identify('hash'), None) + self.assertRaises(KeyError, cc.identify, 'hash', required=True) + + # bad category values + self.assertRaises(TypeError, cc.identify, None, category=1) + + def test_45_verify(self): + """test verify() scheme kwd""" + handlers = ["md5_crypt", "des_crypt", "bsdi_crypt"] + cc = CryptContext(handlers, bsdi_crypt__default_rounds=5) + + h = hash.md5_crypt.hash("test") + + # check base verify + self.assertTrue(cc.verify("test", h)) + self.assertTrue(not cc.verify("notest", h)) + + # check verify using right alg + self.assertTrue(cc.verify('test', h, scheme='md5_crypt')) + self.assertTrue(not cc.verify('notest', h, scheme='md5_crypt')) + + # check verify using wrong alg + self.assertRaises(ValueError, cc.verify, 'test', h, scheme='bsdi_crypt') + + #-------------------------------------------------------------- + # border cases + #-------------------------------------------------------------- + + # unknown hash should throw error + self.assertRaises(ValueError, cc.verify, 'stub', '$6$232323123$1287319827') + + # rejects non-string secrets + cc = CryptContext(["des_crypt"]) + h = refhash = cc.hash('stub') + for secret, kwds in self.nonstring_vectors: + self.assertRaises(TypeError, cc.verify, secret, h, **kwds) + + # always treat hash=None as False + self.assertFalse(cc.verify(secret, None)) + + # rejects non-string hashes + cc = CryptContext(["des_crypt"]) + for h, kwds in self.nonstring_vectors: + if h is None: + continue + self.assertRaises(TypeError, cc.verify, 'secret', h, **kwds) + + # throws error without schemes + self.assertRaises(KeyError, CryptContext().verify, 'secret', 'hash') + + # bad scheme values + self.assertRaises(KeyError, cc.verify, 'secret', refhash, scheme="fake") # XXX: should this be ValueError? + self.assertRaises(TypeError, cc.verify, 'secret', refhash, scheme=1) + + # bad category values + self.assertRaises(TypeError, cc.verify, 'secret', refhash, category=1) + + def test_46_needs_update(self): + """test needs_update() method""" + cc = CryptContext(**self.sample_4_dict) + + # check deprecated scheme + self.assertTrue(cc.needs_update('9XXD4trGYeGJA')) + self.assertFalse(cc.needs_update('$1$J8HC2RCr$HcmM.7NxB2weSvlw2FgzU0')) + + # check min rounds + self.assertTrue(cc.needs_update('$5$rounds=1999$jD81UCoo.zI.UETs$Y7qSTQ6mTiU9qZB4fRr43wRgQq4V.5AAf7F97Pzxey/')) + self.assertFalse(cc.needs_update('$5$rounds=2000$228SSRje04cnNCaQ$YGV4RYu.5sNiBvorQDlO0WWQjyJVGKBcJXz3OtyQ2u8')) + + # check max rounds + self.assertFalse(cc.needs_update('$5$rounds=3000$fS9iazEwTKi7QPW4$VasgBC8FqlOvD7x2HhABaMXCTh9jwHclPA9j5YQdns.')) + self.assertTrue(cc.needs_update('$5$rounds=3001$QlFHHifXvpFX4PLs$/0ekt7lSs/lOikSerQ0M/1porEHxYq7W/2hdFpxA3fA')) + + #-------------------------------------------------------------- + # test hash.needs_update() interface + #-------------------------------------------------------------- + check_state = [] + class dummy(uh.StaticHandler): + name = 'dummy' + _hash_prefix = '@' + + @classmethod + def needs_update(cls, hash, secret=None): + check_state.append((hash, secret)) + return secret == "nu" + + def _calc_checksum(self, secret): + from hashlib import md5 + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + return str_to_uascii(md5(secret).hexdigest()) + + # calling needs_update should query callback + ctx = CryptContext([dummy]) + hash = refhash = dummy.hash("test") + self.assertFalse(ctx.needs_update(hash)) + self.assertEqual(check_state, [(hash,None)]) + del check_state[:] + + # now with a password + self.assertFalse(ctx.needs_update(hash, secret='bob')) + self.assertEqual(check_state, [(hash,'bob')]) + del check_state[:] + + # now when it returns True + self.assertTrue(ctx.needs_update(hash, secret='nu')) + self.assertEqual(check_state, [(hash,'nu')]) + del check_state[:] + + #-------------------------------------------------------------- + # border cases + #-------------------------------------------------------------- + + # rejects non-string hashes + cc = CryptContext(["des_crypt"]) + for hash, kwds in self.nonstring_vectors: + self.assertRaises(TypeError, cc.needs_update, hash, **kwds) + + # throws error without schemes + self.assertRaises(KeyError, CryptContext().needs_update, 'hash') + + # bad scheme values + self.assertRaises(KeyError, cc.needs_update, refhash, scheme="fake") # XXX: should this be ValueError? + self.assertRaises(TypeError, cc.needs_update, refhash, scheme=1) + + # bad category values + self.assertRaises(TypeError, cc.needs_update, refhash, category=1) + + def test_47_verify_and_update(self): + """test verify_and_update()""" + cc = CryptContext(**self.sample_4_dict) + + # create some hashes + h1 = cc.handler("des_crypt").hash("password") + h2 = cc.handler("sha256_crypt").hash("password") + + # check bad password, deprecated hash + ok, new_hash = cc.verify_and_update("wrongpass", h1) + self.assertFalse(ok) + self.assertIs(new_hash, None) + + # check bad password, good hash + ok, new_hash = cc.verify_and_update("wrongpass", h2) + self.assertFalse(ok) + self.assertIs(new_hash, None) + + # check right password, deprecated hash + ok, new_hash = cc.verify_and_update("password", h1) + self.assertTrue(ok) + self.assertTrue(cc.identify(new_hash), "sha256_crypt") + + # check right password, good hash + ok, new_hash = cc.verify_and_update("password", h2) + self.assertTrue(ok) + self.assertIs(new_hash, None) + + #-------------------------------------------------------------- + # border cases + #-------------------------------------------------------------- + + # rejects non-string secrets + cc = CryptContext(["des_crypt"]) + hash = refhash = cc.hash('stub') + for secret, kwds in self.nonstring_vectors: + self.assertRaises(TypeError, cc.verify_and_update, secret, hash, **kwds) + + # always treat hash=None as False + self.assertEqual(cc.verify_and_update(secret, None), (False, None)) + + # rejects non-string hashes + cc = CryptContext(["des_crypt"]) + for hash, kwds in self.nonstring_vectors: + if hash is None: + continue + self.assertRaises(TypeError, cc.verify_and_update, 'secret', hash, **kwds) + + # throws error without schemes + self.assertRaises(KeyError, CryptContext().verify_and_update, 'secret', 'hash') + + # bad scheme values + self.assertRaises(KeyError, cc.verify_and_update, 'secret', refhash, scheme="fake") # XXX: should this be ValueError? + self.assertRaises(TypeError, cc.verify_and_update, 'secret', refhash, scheme=1) + + # bad category values + self.assertRaises(TypeError, cc.verify_and_update, 'secret', refhash, category=1) + + def test_48_context_kwds(self): + """hash(), verify(), and verify_and_update() -- discard unused context keywords""" + + # setup test case + # NOTE: postgres_md5 hash supports 'user' context kwd, which is used for this test. + from passlib.hash import des_crypt, md5_crypt, postgres_md5 + des_hash = des_crypt.hash("stub") + pg_root_hash = postgres_md5.hash("stub", user="root") + pg_admin_hash = postgres_md5.hash("stub", user="admin") + + #------------------------------------------------------------ + # case 1: contextual kwds not supported by any hash in CryptContext + #------------------------------------------------------------ + cc1 = CryptContext([des_crypt, md5_crypt]) + self.assertEqual(cc1.context_kwds, set()) + + # des_scrypt should work w/o any contextual kwds + self.assertTrue(des_crypt.identify(cc1.hash("stub")), "des_crypt") + self.assertTrue(cc1.verify("stub", des_hash)) + self.assertEqual(cc1.verify_and_update("stub", des_hash), (True, None)) + + # des_crypt should throw error due to unknown context keyword + with self.assertWarningList(["passing settings to.*is deprecated"]): + self.assertRaises(TypeError, cc1.hash, "stub", user="root") + self.assertRaises(TypeError, cc1.verify, "stub", des_hash, user="root") + self.assertRaises(TypeError, cc1.verify_and_update, "stub", des_hash, user="root") + + #------------------------------------------------------------ + # case 2: at least one contextual kwd supported by non-default hash + #------------------------------------------------------------ + cc2 = CryptContext([des_crypt, postgres_md5]) + self.assertEqual(cc2.context_kwds, set(["user"])) + + # verify des_crypt works w/o "user" kwd + self.assertTrue(des_crypt.identify(cc2.hash("stub")), "des_crypt") + self.assertTrue(cc2.verify("stub", des_hash)) + self.assertEqual(cc2.verify_and_update("stub", des_hash), (True, None)) + + # verify des_crypt ignores "user" kwd + self.assertTrue(des_crypt.identify(cc2.hash("stub", user="root")), "des_crypt") + self.assertTrue(cc2.verify("stub", des_hash, user="root")) + self.assertEqual(cc2.verify_and_update("stub", des_hash, user="root"), (True, None)) + + # verify error with unknown kwd + with self.assertWarningList(["passing settings to.*is deprecated"]): + self.assertRaises(TypeError, cc2.hash, "stub", badkwd="root") + self.assertRaises(TypeError, cc2.verify, "stub", des_hash, badkwd="root") + self.assertRaises(TypeError, cc2.verify_and_update, "stub", des_hash, badkwd="root") + + #------------------------------------------------------------ + # case 3: at least one contextual kwd supported by default hash + #------------------------------------------------------------ + cc3 = CryptContext([postgres_md5, des_crypt], deprecated="auto") + self.assertEqual(cc3.context_kwds, set(["user"])) + + # postgres_md5 should have error w/o context kwd + self.assertRaises(TypeError, cc3.hash, "stub") + self.assertRaises(TypeError, cc3.verify, "stub", pg_root_hash) + self.assertRaises(TypeError, cc3.verify_and_update, "stub", pg_root_hash) + + # postgres_md5 should work w/ context kwd + self.assertEqual(cc3.hash("stub", user="root"), pg_root_hash) + self.assertTrue(cc3.verify("stub", pg_root_hash, user="root")) + self.assertEqual(cc3.verify_and_update("stub", pg_root_hash, user="root"), (True, None)) + + # verify_and_update() should fail against wrong user + self.assertEqual(cc3.verify_and_update("stub", pg_root_hash, user="admin"), (False, None)) + + # verify_and_update() should pass all context kwds through when rehashing + self.assertEqual(cc3.verify_and_update("stub", des_hash, user="root"), + (True, pg_root_hash)) + + #=================================================================== + # rounds options + #=================================================================== + + # TODO: now that rounds generation has moved out of _CryptRecord to HasRounds, + # this should just test that we're passing right options to handler.using(), + # and that resulting handler has right settings. + # Can then just let HasRounds tests (which are a copy of this) deal with things. + + # NOTE: the follow tests check how _CryptRecord handles + # the min/max/default/vary_rounds options, via the output of + # genconfig(). it's assumed hash() takes the same codepath. + + def test_50_rounds_limits(self): + """test rounds limits""" + cc = CryptContext(schemes=["sha256_crypt"], + sha256_crypt__min_rounds=2000, + sha256_crypt__max_rounds=3000, + sha256_crypt__default_rounds=2500, + ) + + # stub digest returned by sha256_crypt's genconfig calls.. + STUB = '...........................................' + + #-------------------------------------------------- + # settings should have been applied to custom handler, + # it should take care of the rest + #-------------------------------------------------- + custom_handler = cc._get_record("sha256_crypt", None) + self.assertEqual(custom_handler.min_desired_rounds, 2000) + self.assertEqual(custom_handler.max_desired_rounds, 3000) + self.assertEqual(custom_handler.default_rounds, 2500) + + #-------------------------------------------------- + # min_rounds + #-------------------------------------------------- + + # set below handler minimum + with self.assertWarningList([PasslibHashWarning]*2): + c2 = cc.copy(sha256_crypt__min_rounds=500, sha256_crypt__max_rounds=None, + sha256_crypt__default_rounds=500) + self.assertEqual(c2.genconfig(salt="nacl"), "$5$rounds=1000$nacl$" + STUB) + + # below policy minimum + # NOTE: formerly issued a warning in passlib 1.6, now just a wrapper for .replace() + with self.assertWarningList([]): + self.assertEqual( + cc.genconfig(rounds=1999, salt="nacl"), '$5$rounds=1999$nacl$' + STUB) + + # equal to policy minimum + self.assertEqual( + cc.genconfig(rounds=2000, salt="nacl"), '$5$rounds=2000$nacl$' + STUB) + + # above policy minimum + self.assertEqual( + cc.genconfig(rounds=2001, salt="nacl"), '$5$rounds=2001$nacl$' + STUB) + + #-------------------------------------------------- + # max rounds + #-------------------------------------------------- + + # set above handler max + with self.assertWarningList([PasslibHashWarning]*2): + c2 = cc.copy(sha256_crypt__max_rounds=int(1e9)+500, sha256_crypt__min_rounds=None, + sha256_crypt__default_rounds=int(1e9)+500) + + self.assertEqual(c2.genconfig(salt="nacl"), "$5$rounds=999999999$nacl$" + STUB) + + # above policy max + # NOTE: formerly issued a warning in passlib 1.6, now just a wrapper for .using() + with self.assertWarningList([]): + self.assertEqual( + cc.genconfig(rounds=3001, salt="nacl"), '$5$rounds=3001$nacl$' + STUB) + + # equal policy max + self.assertEqual( + cc.genconfig(rounds=3000, salt="nacl"), '$5$rounds=3000$nacl$' + STUB) + + # below policy max + self.assertEqual( + cc.genconfig(rounds=2999, salt="nacl"), '$5$rounds=2999$nacl$' + STUB) + + #-------------------------------------------------- + # default_rounds + #-------------------------------------------------- + + # explicit default rounds + self.assertEqual(cc.genconfig(salt="nacl"), '$5$rounds=2500$nacl$' + STUB) + + # fallback default rounds - use handler's + df = hash.sha256_crypt.default_rounds + c2 = cc.copy(sha256_crypt__default_rounds=None, sha256_crypt__max_rounds=df<<1) + self.assertEqual(c2.genconfig(salt="nacl"), '$5$rounds=%d$nacl$%s' % (df, STUB)) + + # fallback default rounds - use handler's, but clipped to max rounds + c2 = cc.copy(sha256_crypt__default_rounds=None, sha256_crypt__max_rounds=3000) + self.assertEqual(c2.genconfig(salt="nacl"), '$5$rounds=3000$nacl$' + STUB) + + # TODO: test default falls back to mx / mn if handler has no default. + + # default rounds - out of bounds + self.assertRaises(ValueError, cc.copy, sha256_crypt__default_rounds=1999) + cc.copy(sha256_crypt__default_rounds=2000) + cc.copy(sha256_crypt__default_rounds=3000) + self.assertRaises(ValueError, cc.copy, sha256_crypt__default_rounds=3001) + + #-------------------------------------------------- + # border cases + #-------------------------------------------------- + + # invalid min/max bounds + c2 = CryptContext(schemes=["sha256_crypt"]) + # NOTE: as of v1.7, these are clipped w/ a warning instead... + # self.assertRaises(ValueError, c2.copy, sha256_crypt__min_rounds=-1) + # self.assertRaises(ValueError, c2.copy, sha256_crypt__max_rounds=-1) + self.assertRaises(ValueError, c2.copy, sha256_crypt__min_rounds=2000, + sha256_crypt__max_rounds=1999) + + # test bad values + self.assertRaises(ValueError, CryptContext, sha256_crypt__min_rounds='x') + self.assertRaises(ValueError, CryptContext, sha256_crypt__max_rounds='x') + self.assertRaises(ValueError, CryptContext, all__vary_rounds='x') + self.assertRaises(ValueError, CryptContext, sha256_crypt__default_rounds='x') + + # test bad types rejected + bad = datetime.datetime.now() # picked cause can't be compared to int + self.assertRaises(TypeError, CryptContext, "sha256_crypt", sha256_crypt__min_rounds=bad) + self.assertRaises(TypeError, CryptContext, "sha256_crypt", sha256_crypt__max_rounds=bad) + self.assertRaises(TypeError, CryptContext, "sha256_crypt", all__vary_rounds=bad) + self.assertRaises(TypeError, CryptContext, "sha256_crypt", sha256_crypt__default_rounds=bad) + + def test_51_linear_vary_rounds(self): + """test linear vary rounds""" + cc = CryptContext(schemes=["sha256_crypt"], + sha256_crypt__min_rounds=1995, + sha256_crypt__max_rounds=2005, + sha256_crypt__default_rounds=2000, + ) + + # test negative + self.assertRaises(ValueError, cc.copy, all__vary_rounds=-1) + self.assertRaises(ValueError, cc.copy, all__vary_rounds="-1%") + self.assertRaises(ValueError, cc.copy, all__vary_rounds="101%") + + # test static + c2 = cc.copy(all__vary_rounds=0) + self.assertEqual(c2._get_record("sha256_crypt", None).vary_rounds, 0) + self.assert_rounds_range(c2, "sha256_crypt", 2000, 2000) + + c2 = cc.copy(all__vary_rounds="0%") + self.assertEqual(c2._get_record("sha256_crypt", None).vary_rounds, 0) + self.assert_rounds_range(c2, "sha256_crypt", 2000, 2000) + + # test absolute + c2 = cc.copy(all__vary_rounds=1) + self.assertEqual(c2._get_record("sha256_crypt", None).vary_rounds, 1) + self.assert_rounds_range(c2, "sha256_crypt", 1999, 2001) + c2 = cc.copy(all__vary_rounds=100) + self.assertEqual(c2._get_record("sha256_crypt", None).vary_rounds, 100) + self.assert_rounds_range(c2, "sha256_crypt", 1995, 2005) + + # test relative + c2 = cc.copy(all__vary_rounds="0.1%") + self.assertEqual(c2._get_record("sha256_crypt", None).vary_rounds, 0.001) + self.assert_rounds_range(c2, "sha256_crypt", 1998, 2002) + c2 = cc.copy(all__vary_rounds="100%") + self.assertEqual(c2._get_record("sha256_crypt", None).vary_rounds, 1.0) + self.assert_rounds_range(c2, "sha256_crypt", 1995, 2005) + + def test_52_log2_vary_rounds(self): + """test log2 vary rounds""" + cc = CryptContext(schemes=["bcrypt"], + bcrypt__min_rounds=15, + bcrypt__max_rounds=25, + bcrypt__default_rounds=20, + ) + + # test negative + self.assertRaises(ValueError, cc.copy, all__vary_rounds=-1) + self.assertRaises(ValueError, cc.copy, all__vary_rounds="-1%") + self.assertRaises(ValueError, cc.copy, all__vary_rounds="101%") + + # test static + c2 = cc.copy(all__vary_rounds=0) + self.assertEqual(c2._get_record("bcrypt", None).vary_rounds, 0) + self.assert_rounds_range(c2, "bcrypt", 20, 20) + + c2 = cc.copy(all__vary_rounds="0%") + self.assertEqual(c2._get_record("bcrypt", None).vary_rounds, 0) + self.assert_rounds_range(c2, "bcrypt", 20, 20) + + # test absolute + c2 = cc.copy(all__vary_rounds=1) + self.assertEqual(c2._get_record("bcrypt", None).vary_rounds, 1) + self.assert_rounds_range(c2, "bcrypt", 19, 21) + c2 = cc.copy(all__vary_rounds=100) + self.assertEqual(c2._get_record("bcrypt", None).vary_rounds, 100) + self.assert_rounds_range(c2, "bcrypt", 15, 25) + + # test relative - should shift over at 50% mark + c2 = cc.copy(all__vary_rounds="1%") + self.assertEqual(c2._get_record("bcrypt", None).vary_rounds, 0.01) + self.assert_rounds_range(c2, "bcrypt", 20, 20) + + c2 = cc.copy(all__vary_rounds="49%") + self.assertEqual(c2._get_record("bcrypt", None).vary_rounds, 0.49) + self.assert_rounds_range(c2, "bcrypt", 20, 20) + + c2 = cc.copy(all__vary_rounds="50%") + self.assertEqual(c2._get_record("bcrypt", None).vary_rounds, 0.5) + self.assert_rounds_range(c2, "bcrypt", 19, 20) + + c2 = cc.copy(all__vary_rounds="100%") + self.assertEqual(c2._get_record("bcrypt", None).vary_rounds, 1.0) + self.assert_rounds_range(c2, "bcrypt", 15, 21) + + def assert_rounds_range(self, context, scheme, lower, upper): + """helper to check vary_rounds covers specified range""" + # NOTE: this runs enough times the min and max *should* be hit, + # though there's a faint chance it will randomly fail. + handler = context.handler(scheme) + salt = handler.default_salt_chars[0:1] * handler.max_salt_size + seen = set() + for i in irange(300): + h = context.genconfig(scheme, salt=salt) + r = handler.from_string(h).rounds + seen.add(r) + self.assertEqual(min(seen), lower, "vary_rounds had wrong lower limit:") + self.assertEqual(max(seen), upper, "vary_rounds had wrong upper limit:") + + #=================================================================== + # harden_verify / min_verify_time + #=================================================================== + def test_harden_verify_parsing(self): + """harden_verify -- parsing""" + warnings.filterwarnings("ignore", ".*harden_verify.*", + category=DeprecationWarning) + + # valid values + ctx = CryptContext(schemes=["sha256_crypt"]) + self.assertEqual(ctx.harden_verify, None) + self.assertEqual(ctx.using(harden_verify="").harden_verify, None) + self.assertEqual(ctx.using(harden_verify="true").harden_verify, None) + self.assertEqual(ctx.using(harden_verify="false").harden_verify, None) + + def test_dummy_verify(self): + """ + dummy_verify() method + """ + # check dummy_verify() takes expected time + expected = 0.05 + accuracy = 0.2 + handler = DelayHash.using() + handler.delay = expected + ctx = CryptContext(schemes=[handler]) + ctx.dummy_verify() # prime the memoized helpers + elapsed, _ = time_call(ctx.dummy_verify) + self.assertAlmostEqual(elapsed, expected, delta=expected * accuracy) + + # TODO: test dummy_verify() invoked by .verify() when hash is None, + # and same for .verify_and_update() + + #=================================================================== + # feature tests + #=================================================================== + def test_61_autodeprecate(self): + """test deprecated='auto' is handled correctly""" + + def getstate(ctx, category=None): + return [ctx.handler(scheme, category).deprecated for scheme in ctx.schemes()] + + # correctly reports default + ctx = CryptContext("sha256_crypt,md5_crypt,des_crypt", deprecated="auto") + self.assertEqual(getstate(ctx, None), [False, True, True]) + self.assertEqual(getstate(ctx, "admin"), [False, True, True]) + + # correctly reports changed default + ctx.update(default="md5_crypt") + self.assertEqual(getstate(ctx, None), [True, False, True]) + self.assertEqual(getstate(ctx, "admin"), [True, False, True]) + + # category default is handled correctly + ctx.update(admin__context__default="des_crypt") + self.assertEqual(getstate(ctx, None), [True, False, True]) + self.assertEqual(getstate(ctx, "admin"), [True, True, False]) + + # handles 1 scheme + ctx = CryptContext(["sha256_crypt"], deprecated="auto") + self.assertEqual(getstate(ctx, None), [False]) + self.assertEqual(getstate(ctx, "admin"), [False]) + + # disallow auto & other deprecated schemes at same time. + self.assertRaises(ValueError, CryptContext, "sha256_crypt,md5_crypt", + deprecated="auto,md5_crypt") + self.assertRaises(ValueError, CryptContext, "sha256_crypt,md5_crypt", + deprecated="md5_crypt,auto") + + def test_disabled_hashes(self): + """disabled hash support""" + # + # init ref info + # + from passlib.exc import UnknownHashError + from passlib.hash import md5_crypt, unix_disabled + + ctx = CryptContext(["des_crypt"]) + ctx2 = CryptContext(["des_crypt", "unix_disabled"]) + h_ref = ctx.hash("foo") + h_other = md5_crypt.hash('foo') + + # + # ctx.disable() + # + + # test w/o disabled hash support + self.assertRaisesRegex(RuntimeError, "no disabled hasher present", + ctx.disable) + self.assertRaisesRegex(RuntimeError, "no disabled hasher present", + ctx.disable, h_ref) + self.assertRaisesRegex(RuntimeError, "no disabled hasher present", + ctx.disable, h_other) + + # test w/ disabled hash support + h_dis = ctx2.disable() + self.assertEqual(h_dis, unix_disabled.default_marker) + h_dis_ref = ctx2.disable(h_ref) + self.assertEqual(h_dis_ref, unix_disabled.default_marker + h_ref) + + h_dis_other = ctx2.disable(h_other) + self.assertEqual(h_dis_other, unix_disabled.default_marker + h_other) + + # don't double-wrap existing disabled hash + self.assertEqual(ctx2.disable(h_dis_ref), h_dis_ref) + + # + # ctx.is_enabled() + # + + # test w/o disabled hash support + self.assertTrue(ctx.is_enabled(h_ref)) + self.assertRaises(UnknownHashError, ctx.is_enabled, h_other) + self.assertRaises(UnknownHashError, ctx.is_enabled, h_dis) + self.assertRaises(UnknownHashError, ctx.is_enabled, h_dis_ref) + + # test w/ disabled hash support + self.assertTrue(ctx2.is_enabled(h_ref)) + self.assertRaises(UnknownHashError, ctx.is_enabled, h_other) + self.assertFalse(ctx2.is_enabled(h_dis)) + self.assertFalse(ctx2.is_enabled(h_dis_ref)) + + # + # ctx.enable() + # + + # test w/o disabled hash support + self.assertRaises(UnknownHashError, ctx.enable, "") + self.assertRaises(TypeError, ctx.enable, None) + self.assertEqual(ctx.enable(h_ref), h_ref) + self.assertRaises(UnknownHashError, ctx.enable, h_other) + self.assertRaises(UnknownHashError, ctx.enable, h_dis) + self.assertRaises(UnknownHashError, ctx.enable, h_dis_ref) + + # test w/ disabled hash support + self.assertRaises(UnknownHashError, ctx.enable, "") + self.assertRaises(TypeError, ctx2.enable, None) + self.assertEqual(ctx2.enable(h_ref), h_ref) + self.assertRaises(UnknownHashError, ctx2.enable, h_other) + self.assertRaisesRegex(ValueError, "cannot restore original hash", + ctx2.enable, h_dis) + self.assertEqual(ctx2.enable(h_dis_ref), h_ref) + + #=================================================================== + # eoc + #=================================================================== + +import hashlib, time + +class DelayHash(uh.StaticHandler): + """dummy hasher which delays by specified amount""" + name = "delay_hash" + checksum_chars = uh.LOWER_HEX_CHARS + checksum_size = 40 + delay = 0 + _hash_prefix = u("$x$") + + def _calc_checksum(self, secret): + time.sleep(self.delay) + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + return str_to_uascii(hashlib.sha1(b"prefix" + secret).hexdigest()) + +#============================================================================= +# LazyCryptContext +#============================================================================= +class dummy_2(uh.StaticHandler): + name = "dummy_2" + +class LazyCryptContextTest(TestCase): + descriptionPrefix = "LazyCryptContext" + + def setUp(self): + # make sure this isn't registered before OR after + unload_handler_name("dummy_2") + self.addCleanup(unload_handler_name, "dummy_2") + + def test_kwd_constructor(self): + """test plain kwds""" + self.assertFalse(has_crypt_handler("dummy_2")) + register_crypt_handler_path("dummy_2", "passlib.tests.test_context") + + cc = LazyCryptContext(iter(["dummy_2", "des_crypt"]), deprecated=["des_crypt"]) + + self.assertFalse(has_crypt_handler("dummy_2", True)) + + self.assertEqual(cc.schemes(), ("dummy_2", "des_crypt")) + self.assertTrue(cc.handler("des_crypt").deprecated) + + self.assertTrue(has_crypt_handler("dummy_2", True)) + + def test_callable_constructor(self): + self.assertFalse(has_crypt_handler("dummy_2")) + register_crypt_handler_path("dummy_2", "passlib.tests.test_context") + + def onload(flag=False): + self.assertTrue(flag) + return dict(schemes=iter(["dummy_2", "des_crypt"]), deprecated=["des_crypt"]) + + cc = LazyCryptContext(onload=onload, flag=True) + + self.assertFalse(has_crypt_handler("dummy_2", True)) + + self.assertEqual(cc.schemes(), ("dummy_2", "des_crypt")) + self.assertTrue(cc.handler("des_crypt").deprecated) + + self.assertTrue(has_crypt_handler("dummy_2", True)) + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/test_context_deprecated.py b/ansible/lib/python3.11/site-packages/passlib/tests/test_context_deprecated.py new file mode 100644 index 000000000..0f76624c7 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/tests/test_context_deprecated.py @@ -0,0 +1,743 @@ +"""tests for passlib.context + +this file is a clone of the 1.5 test_context.py, +containing the tests using the legacy CryptPolicy api. +it's being preserved here to ensure the old api doesn't break +(until Passlib 1.8, when this and the legacy api will be removed). +""" +#============================================================================= +# imports +#============================================================================= +from __future__ import with_statement +# core +from logging import getLogger +import os +import warnings +# site +try: + from pkg_resources import resource_filename +except ImportError: + resource_filename = None +# pkg +from passlib import hash +from passlib.context import CryptContext, CryptPolicy, LazyCryptContext +from passlib.utils import to_bytes, to_unicode +import passlib.utils.handlers as uh +from passlib.tests.utils import TestCase, set_file +from passlib.registry import (register_crypt_handler_path, + _has_crypt_handler as has_crypt_handler, + _unload_handler_name as unload_handler_name, + ) +# module +log = getLogger(__name__) + +#============================================================================= +# +#============================================================================= +class CryptPolicyTest(TestCase): + """test CryptPolicy object""" + + # TODO: need to test user categories w/in all this + + descriptionPrefix = "CryptPolicy" + + #=================================================================== + # sample crypt policies used for testing + #=================================================================== + + #--------------------------------------------------------------- + # sample 1 - average config file + #--------------------------------------------------------------- + # NOTE: copy of this is stored in file passlib/tests/sample_config_1s.cfg + sample_config_1s = """\ +[passlib] +schemes = des_crypt, md5_crypt, bsdi_crypt, sha512_crypt +default = md5_crypt +all.vary_rounds = 10%% +bsdi_crypt.max_rounds = 30000 +bsdi_crypt.default_rounds = 25000 +sha512_crypt.max_rounds = 50000 +sha512_crypt.min_rounds = 40000 +""" + sample_config_1s_path = os.path.abspath(os.path.join( + os.path.dirname(__file__), "sample_config_1s.cfg")) + if not os.path.exists(sample_config_1s_path) and resource_filename: + # in case we're zipped up in an egg. + sample_config_1s_path = resource_filename("passlib.tests", + "sample_config_1s.cfg") + + # make sure sample_config_1s uses \n linesep - tests rely on this + assert sample_config_1s.startswith("[passlib]\nschemes") + + sample_config_1pd = dict( + schemes = [ "des_crypt", "md5_crypt", "bsdi_crypt", "sha512_crypt"], + default = "md5_crypt", + # NOTE: not maintaining backwards compat for rendering to "10%" + all__vary_rounds = 0.1, + bsdi_crypt__max_rounds = 30000, + bsdi_crypt__default_rounds = 25000, + sha512_crypt__max_rounds = 50000, + sha512_crypt__min_rounds = 40000, + ) + + sample_config_1pid = { + "schemes": "des_crypt, md5_crypt, bsdi_crypt, sha512_crypt", + "default": "md5_crypt", + # NOTE: not maintaining backwards compat for rendering to "10%" + "all.vary_rounds": 0.1, + "bsdi_crypt.max_rounds": 30000, + "bsdi_crypt.default_rounds": 25000, + "sha512_crypt.max_rounds": 50000, + "sha512_crypt.min_rounds": 40000, + } + + sample_config_1prd = dict( + schemes = [ hash.des_crypt, hash.md5_crypt, hash.bsdi_crypt, hash.sha512_crypt], + default = "md5_crypt", # NOTE: passlib <= 1.5 was handler obj. + # NOTE: not maintaining backwards compat for rendering to "10%" + all__vary_rounds = 0.1, + bsdi_crypt__max_rounds = 30000, + bsdi_crypt__default_rounds = 25000, + sha512_crypt__max_rounds = 50000, + sha512_crypt__min_rounds = 40000, + ) + + #--------------------------------------------------------------- + # sample 2 - partial policy & result of overlay on sample 1 + #--------------------------------------------------------------- + sample_config_2s = """\ +[passlib] +bsdi_crypt.min_rounds = 29000 +bsdi_crypt.max_rounds = 35000 +bsdi_crypt.default_rounds = 31000 +sha512_crypt.min_rounds = 45000 +""" + + sample_config_2pd = dict( + # using this to test full replacement of existing options + bsdi_crypt__min_rounds = 29000, + bsdi_crypt__max_rounds = 35000, + bsdi_crypt__default_rounds = 31000, + # using this to test partial replacement of existing options + sha512_crypt__min_rounds=45000, + ) + + sample_config_12pd = dict( + schemes = [ "des_crypt", "md5_crypt", "bsdi_crypt", "sha512_crypt"], + default = "md5_crypt", + # NOTE: not maintaining backwards compat for rendering to "10%" + all__vary_rounds = 0.1, + bsdi_crypt__min_rounds = 29000, + bsdi_crypt__max_rounds = 35000, + bsdi_crypt__default_rounds = 31000, + sha512_crypt__max_rounds = 50000, + sha512_crypt__min_rounds=45000, + ) + + #--------------------------------------------------------------- + # sample 3 - just changing default + #--------------------------------------------------------------- + sample_config_3pd = dict( + default="sha512_crypt", + ) + + sample_config_123pd = dict( + schemes = [ "des_crypt", "md5_crypt", "bsdi_crypt", "sha512_crypt"], + default = "sha512_crypt", + # NOTE: not maintaining backwards compat for rendering to "10%" + all__vary_rounds = 0.1, + bsdi_crypt__min_rounds = 29000, + bsdi_crypt__max_rounds = 35000, + bsdi_crypt__default_rounds = 31000, + sha512_crypt__max_rounds = 50000, + sha512_crypt__min_rounds=45000, + ) + + #--------------------------------------------------------------- + # sample 4 - category specific + #--------------------------------------------------------------- + sample_config_4s = """ +[passlib] +schemes = sha512_crypt +all.vary_rounds = 10%% +default.sha512_crypt.max_rounds = 20000 +admin.all.vary_rounds = 5%% +admin.sha512_crypt.max_rounds = 40000 +""" + + sample_config_4pd = dict( + schemes = [ "sha512_crypt" ], + # NOTE: not maintaining backwards compat for rendering to "10%" + all__vary_rounds = 0.1, + sha512_crypt__max_rounds = 20000, + # NOTE: not maintaining backwards compat for rendering to "5%" + admin__all__vary_rounds = 0.05, + admin__sha512_crypt__max_rounds = 40000, + ) + + #--------------------------------------------------------------- + # sample 5 - to_string & deprecation testing + #--------------------------------------------------------------- + sample_config_5s = sample_config_1s + """\ +deprecated = des_crypt +admin__context__deprecated = des_crypt, bsdi_crypt +""" + + sample_config_5pd = sample_config_1pd.copy() + sample_config_5pd.update( + deprecated = [ "des_crypt" ], + admin__context__deprecated = [ "des_crypt", "bsdi_crypt" ], + ) + + sample_config_5pid = sample_config_1pid.copy() + sample_config_5pid.update({ + "deprecated": "des_crypt", + "admin.context.deprecated": "des_crypt, bsdi_crypt", + }) + + sample_config_5prd = sample_config_1prd.copy() + sample_config_5prd.update({ + # XXX: should deprecated return the actual handlers in this case? + # would have to modify how policy stores info, for one. + "deprecated": ["des_crypt"], + "admin__context__deprecated": ["des_crypt", "bsdi_crypt"], + }) + + #=================================================================== + # constructors + #=================================================================== + def setUp(self): + TestCase.setUp(self) + warnings.filterwarnings("ignore", + r"The CryptPolicy class has been deprecated") + warnings.filterwarnings("ignore", + r"the method.*hash_needs_update.*is deprecated") + warnings.filterwarnings("ignore", "The 'all' scheme is deprecated.*") + warnings.filterwarnings("ignore", "bsdi_crypt rounds should be odd") + + def test_00_constructor(self): + """test CryptPolicy() constructor""" + policy = CryptPolicy(**self.sample_config_1pd) + self.assertEqual(policy.to_dict(), self.sample_config_1pd) + + policy = CryptPolicy(self.sample_config_1pd) + self.assertEqual(policy.to_dict(), self.sample_config_1pd) + + self.assertRaises(TypeError, CryptPolicy, {}, {}) + self.assertRaises(TypeError, CryptPolicy, {}, dummy=1) + + # check key with too many separators is rejected + self.assertRaises(TypeError, CryptPolicy, + schemes = [ "des_crypt", "md5_crypt", "bsdi_crypt", "sha512_crypt"], + bad__key__bsdi_crypt__max_rounds = 30000, + ) + + # check nameless handler rejected + class nameless(uh.StaticHandler): + name = None + self.assertRaises(ValueError, CryptPolicy, schemes=[nameless]) + + # check scheme must be name or crypt handler + self.assertRaises(TypeError, CryptPolicy, schemes=[uh.StaticHandler]) + + # check name conflicts are rejected + class dummy_1(uh.StaticHandler): + name = 'dummy_1' + self.assertRaises(KeyError, CryptPolicy, schemes=[dummy_1, dummy_1]) + + # with unknown deprecated value + self.assertRaises(KeyError, CryptPolicy, + schemes=['des_crypt'], + deprecated=['md5_crypt']) + + # with unknown default value + self.assertRaises(KeyError, CryptPolicy, + schemes=['des_crypt'], + default='md5_crypt') + + def test_01_from_path_simple(self): + """test CryptPolicy.from_path() constructor""" + # NOTE: this is separate so it can also run under GAE + + # test preset stored in existing file + path = self.sample_config_1s_path + policy = CryptPolicy.from_path(path) + self.assertEqual(policy.to_dict(), self.sample_config_1pd) + + # test if path missing + self.assertRaises(EnvironmentError, CryptPolicy.from_path, path + 'xxx') + + def test_01_from_path(self): + """test CryptPolicy.from_path() constructor with encodings""" + path = self.mktemp() + + # test "\n" linesep + set_file(path, self.sample_config_1s) + policy = CryptPolicy.from_path(path) + self.assertEqual(policy.to_dict(), self.sample_config_1pd) + + # test "\r\n" linesep + set_file(path, self.sample_config_1s.replace("\n","\r\n")) + policy = CryptPolicy.from_path(path) + self.assertEqual(policy.to_dict(), self.sample_config_1pd) + + # test with custom encoding + uc2 = to_bytes(self.sample_config_1s, "utf-16", source_encoding="utf-8") + set_file(path, uc2) + policy = CryptPolicy.from_path(path, encoding="utf-16") + self.assertEqual(policy.to_dict(), self.sample_config_1pd) + + def test_02_from_string(self): + """test CryptPolicy.from_string() constructor""" + # test "\n" linesep + policy = CryptPolicy.from_string(self.sample_config_1s) + self.assertEqual(policy.to_dict(), self.sample_config_1pd) + + # test "\r\n" linesep + policy = CryptPolicy.from_string( + self.sample_config_1s.replace("\n","\r\n")) + self.assertEqual(policy.to_dict(), self.sample_config_1pd) + + # test with unicode + data = to_unicode(self.sample_config_1s) + policy = CryptPolicy.from_string(data) + self.assertEqual(policy.to_dict(), self.sample_config_1pd) + + # test with non-ascii-compatible encoding + uc2 = to_bytes(self.sample_config_1s, "utf-16", source_encoding="utf-8") + policy = CryptPolicy.from_string(uc2, encoding="utf-16") + self.assertEqual(policy.to_dict(), self.sample_config_1pd) + + # test category specific options + policy = CryptPolicy.from_string(self.sample_config_4s) + self.assertEqual(policy.to_dict(), self.sample_config_4pd) + + def test_03_from_source(self): + """test CryptPolicy.from_source() constructor""" + # pass it a path + policy = CryptPolicy.from_source(self.sample_config_1s_path) + self.assertEqual(policy.to_dict(), self.sample_config_1pd) + + # pass it a string + policy = CryptPolicy.from_source(self.sample_config_1s) + self.assertEqual(policy.to_dict(), self.sample_config_1pd) + + # pass it a dict (NOTE: make a copy to detect in-place modifications) + policy = CryptPolicy.from_source(self.sample_config_1pd.copy()) + self.assertEqual(policy.to_dict(), self.sample_config_1pd) + + # pass it existing policy + p2 = CryptPolicy.from_source(policy) + self.assertIs(policy, p2) + + # pass it something wrong + self.assertRaises(TypeError, CryptPolicy.from_source, 1) + self.assertRaises(TypeError, CryptPolicy.from_source, []) + + def test_04_from_sources(self): + """test CryptPolicy.from_sources() constructor""" + + # pass it empty list + self.assertRaises(ValueError, CryptPolicy.from_sources, []) + + # pass it one-element list + policy = CryptPolicy.from_sources([self.sample_config_1s]) + self.assertEqual(policy.to_dict(), self.sample_config_1pd) + + # pass multiple sources + policy = CryptPolicy.from_sources( + [ + self.sample_config_1s_path, + self.sample_config_2s, + self.sample_config_3pd, + ]) + self.assertEqual(policy.to_dict(), self.sample_config_123pd) + + def test_05_replace(self): + """test CryptPolicy.replace() constructor""" + + p1 = CryptPolicy(**self.sample_config_1pd) + + # check overlaying sample 2 + p2 = p1.replace(**self.sample_config_2pd) + self.assertEqual(p2.to_dict(), self.sample_config_12pd) + + # check repeating overlay makes no change + p2b = p2.replace(**self.sample_config_2pd) + self.assertEqual(p2b.to_dict(), self.sample_config_12pd) + + # check overlaying sample 3 + p3 = p2.replace(self.sample_config_3pd) + self.assertEqual(p3.to_dict(), self.sample_config_123pd) + + def test_06_forbidden(self): + """test CryptPolicy() forbidden kwds""" + + # salt not allowed to be set + self.assertRaises(KeyError, CryptPolicy, + schemes=["des_crypt"], + des_crypt__salt="xx", + ) + self.assertRaises(KeyError, CryptPolicy, + schemes=["des_crypt"], + all__salt="xx", + ) + + # schemes not allowed for category + self.assertRaises(KeyError, CryptPolicy, + schemes=["des_crypt"], + user__context__schemes=["md5_crypt"], + ) + + #=================================================================== + # reading + #=================================================================== + def test_10_has_schemes(self): + """test has_schemes() method""" + + p1 = CryptPolicy(**self.sample_config_1pd) + self.assertTrue(p1.has_schemes()) + + p3 = CryptPolicy(**self.sample_config_3pd) + self.assertTrue(not p3.has_schemes()) + + def test_11_iter_handlers(self): + """test iter_handlers() method""" + + p1 = CryptPolicy(**self.sample_config_1pd) + s = self.sample_config_1prd['schemes'] + self.assertEqual(list(p1.iter_handlers()), s) + + p3 = CryptPolicy(**self.sample_config_3pd) + self.assertEqual(list(p3.iter_handlers()), []) + + def test_12_get_handler(self): + """test get_handler() method""" + + p1 = CryptPolicy(**self.sample_config_1pd) + + # check by name + self.assertIs(p1.get_handler("bsdi_crypt"), hash.bsdi_crypt) + + # check by missing name + self.assertIs(p1.get_handler("sha256_crypt"), None) + self.assertRaises(KeyError, p1.get_handler, "sha256_crypt", required=True) + + # check default + self.assertIs(p1.get_handler(), hash.md5_crypt) + + def test_13_get_options(self): + """test get_options() method""" + + p12 = CryptPolicy(**self.sample_config_12pd) + + self.assertEqual(p12.get_options("bsdi_crypt"),dict( + # NOTE: not maintaining backwards compat for rendering to "10%" + vary_rounds = 0.1, + min_rounds = 29000, + max_rounds = 35000, + default_rounds = 31000, + )) + + self.assertEqual(p12.get_options("sha512_crypt"),dict( + # NOTE: not maintaining backwards compat for rendering to "10%" + vary_rounds = 0.1, + min_rounds = 45000, + max_rounds = 50000, + )) + + p4 = CryptPolicy.from_string(self.sample_config_4s) + self.assertEqual(p4.get_options("sha512_crypt"), dict( + # NOTE: not maintaining backwards compat for rendering to "10%" + vary_rounds=0.1, + max_rounds=20000, + )) + + self.assertEqual(p4.get_options("sha512_crypt", "user"), dict( + # NOTE: not maintaining backwards compat for rendering to "10%" + vary_rounds=0.1, + max_rounds=20000, + )) + + self.assertEqual(p4.get_options("sha512_crypt", "admin"), dict( + # NOTE: not maintaining backwards compat for rendering to "5%" + vary_rounds=0.05, + max_rounds=40000, + )) + + def test_14_handler_is_deprecated(self): + """test handler_is_deprecated() method""" + pa = CryptPolicy(**self.sample_config_1pd) + pb = CryptPolicy(**self.sample_config_5pd) + + self.assertFalse(pa.handler_is_deprecated("des_crypt")) + self.assertFalse(pa.handler_is_deprecated(hash.bsdi_crypt)) + self.assertFalse(pa.handler_is_deprecated("sha512_crypt")) + + self.assertTrue(pb.handler_is_deprecated("des_crypt")) + self.assertFalse(pb.handler_is_deprecated(hash.bsdi_crypt)) + self.assertFalse(pb.handler_is_deprecated("sha512_crypt")) + + # check categories as well + self.assertTrue(pb.handler_is_deprecated("des_crypt", "user")) + self.assertFalse(pb.handler_is_deprecated("bsdi_crypt", "user")) + self.assertTrue(pb.handler_is_deprecated("des_crypt", "admin")) + self.assertTrue(pb.handler_is_deprecated("bsdi_crypt", "admin")) + + # check deprecation is overridden per category + pc = CryptPolicy( + schemes=["md5_crypt", "des_crypt"], + deprecated=["md5_crypt"], + user__context__deprecated=["des_crypt"], + ) + self.assertTrue(pc.handler_is_deprecated("md5_crypt")) + self.assertFalse(pc.handler_is_deprecated("des_crypt")) + self.assertFalse(pc.handler_is_deprecated("md5_crypt", "user")) + self.assertTrue(pc.handler_is_deprecated("des_crypt", "user")) + + def test_15_min_verify_time(self): + """test get_min_verify_time() method""" + # silence deprecation warnings for min verify time + warnings.filterwarnings("ignore", category=DeprecationWarning) + + pa = CryptPolicy() + self.assertEqual(pa.get_min_verify_time(), 0) + self.assertEqual(pa.get_min_verify_time('admin'), 0) + + pb = pa.replace(min_verify_time=.1) + self.assertEqual(pb.get_min_verify_time(), 0) + self.assertEqual(pb.get_min_verify_time('admin'), 0) + + #=================================================================== + # serialization + #=================================================================== + def test_20_iter_config(self): + """test iter_config() method""" + p5 = CryptPolicy(**self.sample_config_5pd) + self.assertEqual(dict(p5.iter_config()), self.sample_config_5pd) + self.assertEqual(dict(p5.iter_config(resolve=True)), self.sample_config_5prd) + self.assertEqual(dict(p5.iter_config(ini=True)), self.sample_config_5pid) + + def test_21_to_dict(self): + """test to_dict() method""" + p5 = CryptPolicy(**self.sample_config_5pd) + self.assertEqual(p5.to_dict(), self.sample_config_5pd) + self.assertEqual(p5.to_dict(resolve=True), self.sample_config_5prd) + + def test_22_to_string(self): + """test to_string() method""" + pa = CryptPolicy(**self.sample_config_5pd) + s = pa.to_string() # NOTE: can't compare string directly, ordering etc may not match + pb = CryptPolicy.from_string(s) + self.assertEqual(pb.to_dict(), self.sample_config_5pd) + + s = pa.to_string(encoding="latin-1") + self.assertIsInstance(s, bytes) + + #=================================================================== + # + #=================================================================== + +#============================================================================= +# CryptContext +#============================================================================= +class CryptContextTest(TestCase): + """test CryptContext class""" + descriptionPrefix = "CryptContext" + + def setUp(self): + TestCase.setUp(self) + warnings.filterwarnings("ignore", + r"CryptContext\(\)\.replace\(\) has been deprecated.*") + warnings.filterwarnings("ignore", + r"The CryptContext ``policy`` keyword has been deprecated.*") + warnings.filterwarnings("ignore", ".*(CryptPolicy|context\.policy).*(has|have) been deprecated.*") + warnings.filterwarnings("ignore", + r"the method.*hash_needs_update.*is deprecated") + + #=================================================================== + # constructor + #=================================================================== + def test_00_constructor(self): + """test constructor""" + # create crypt context using handlers + cc = CryptContext([hash.md5_crypt, hash.bsdi_crypt, hash.des_crypt]) + c,b,a = cc.policy.iter_handlers() + self.assertIs(a, hash.des_crypt) + self.assertIs(b, hash.bsdi_crypt) + self.assertIs(c, hash.md5_crypt) + + # create context using names + cc = CryptContext(["md5_crypt", "bsdi_crypt", "des_crypt"]) + c,b,a = cc.policy.iter_handlers() + self.assertIs(a, hash.des_crypt) + self.assertIs(b, hash.bsdi_crypt) + self.assertIs(c, hash.md5_crypt) + + # policy kwd + policy = cc.policy + cc = CryptContext(policy=policy) + self.assertEqual(cc.to_dict(), policy.to_dict()) + + cc = CryptContext(policy=policy, default="bsdi_crypt") + self.assertNotEqual(cc.to_dict(), policy.to_dict()) + self.assertEqual(cc.to_dict(), dict(schemes=["md5_crypt","bsdi_crypt","des_crypt"], + default="bsdi_crypt")) + + self.assertRaises(TypeError, setattr, cc, 'policy', None) + self.assertRaises(TypeError, CryptContext, policy='x') + + def test_01_replace(self): + """test replace()""" + + cc = CryptContext(["md5_crypt", "bsdi_crypt", "des_crypt"]) + self.assertIs(cc.policy.get_handler(), hash.md5_crypt) + + cc2 = cc.replace() + self.assertIsNot(cc2, cc) + # NOTE: was not able to maintain backward compatibility with this... + ##self.assertIs(cc2.policy, cc.policy) + + cc3 = cc.replace(default="bsdi_crypt") + self.assertIsNot(cc3, cc) + # NOTE: was not able to maintain backward compatibility with this... + ##self.assertIs(cc3.policy, cc.policy) + self.assertIs(cc3.policy.get_handler(), hash.bsdi_crypt) + + def test_02_no_handlers(self): + """test no handlers""" + + # check constructor... + cc = CryptContext() + self.assertRaises(KeyError, cc.identify, 'hash', required=True) + self.assertRaises(KeyError, cc.hash, 'secret') + self.assertRaises(KeyError, cc.verify, 'secret', 'hash') + + # check updating policy after the fact... + cc = CryptContext(['md5_crypt']) + p = CryptPolicy(schemes=[]) + cc.policy = p + + self.assertRaises(KeyError, cc.identify, 'hash', required=True) + self.assertRaises(KeyError, cc.hash, 'secret') + self.assertRaises(KeyError, cc.verify, 'secret', 'hash') + + #=================================================================== + # policy adaptation + #=================================================================== + sample_policy_1 = dict( + schemes = [ "des_crypt", "md5_crypt", "phpass", "bsdi_crypt", + "sha256_crypt"], + deprecated = [ "des_crypt", ], + default = "sha256_crypt", + bsdi_crypt__max_rounds = 30, + bsdi_crypt__default_rounds = 25, + bsdi_crypt__vary_rounds = 0, + sha256_crypt__max_rounds = 3000, + sha256_crypt__min_rounds = 2000, + sha256_crypt__default_rounds = 3000, + phpass__ident = "H", + phpass__default_rounds = 7, + ) + + def test_12_hash_needs_update(self): + """test hash_needs_update() method""" + cc = CryptContext(**self.sample_policy_1) + + # check deprecated scheme + self.assertTrue(cc.hash_needs_update('9XXD4trGYeGJA')) + self.assertFalse(cc.hash_needs_update('$1$J8HC2RCr$HcmM.7NxB2weSvlw2FgzU0')) + + # check min rounds + self.assertTrue(cc.hash_needs_update('$5$rounds=1999$jD81UCoo.zI.UETs$Y7qSTQ6mTiU9qZB4fRr43wRgQq4V.5AAf7F97Pzxey/')) + self.assertFalse(cc.hash_needs_update('$5$rounds=2000$228SSRje04cnNCaQ$YGV4RYu.5sNiBvorQDlO0WWQjyJVGKBcJXz3OtyQ2u8')) + + # check max rounds + self.assertFalse(cc.hash_needs_update('$5$rounds=3000$fS9iazEwTKi7QPW4$VasgBC8FqlOvD7x2HhABaMXCTh9jwHclPA9j5YQdns.')) + self.assertTrue(cc.hash_needs_update('$5$rounds=3001$QlFHHifXvpFX4PLs$/0ekt7lSs/lOikSerQ0M/1porEHxYq7W/2hdFpxA3fA')) + + #=================================================================== + # border cases + #=================================================================== + def test_30_nonstring_hash(self): + """test non-string hash values cause error""" + warnings.filterwarnings("ignore", ".*needs_update.*'scheme' keyword is deprecated.*") + + # + # test hash=None or some other non-string causes TypeError + # and that explicit-scheme code path behaves the same. + # + cc = CryptContext(["des_crypt"]) + for hash, kwds in [ + (None, {}), + # NOTE: 'scheme' kwd is deprecated... + (None, {"scheme": "des_crypt"}), + (1, {}), + ((), {}), + ]: + + self.assertRaises(TypeError, cc.hash_needs_update, hash, **kwds) + + cc2 = CryptContext(["mysql323"]) + self.assertRaises(TypeError, cc2.hash_needs_update, None) + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# LazyCryptContext +#============================================================================= +class dummy_2(uh.StaticHandler): + name = "dummy_2" + +class LazyCryptContextTest(TestCase): + descriptionPrefix = "LazyCryptContext" + + def setUp(self): + TestCase.setUp(self) + + # make sure this isn't registered before OR after + unload_handler_name("dummy_2") + self.addCleanup(unload_handler_name, "dummy_2") + + # silence some warnings + warnings.filterwarnings("ignore", + r"CryptContext\(\)\.replace\(\) has been deprecated") + warnings.filterwarnings("ignore", ".*(CryptPolicy|context\.policy).*(has|have) been deprecated.*") + + def test_kwd_constructor(self): + """test plain kwds""" + self.assertFalse(has_crypt_handler("dummy_2")) + register_crypt_handler_path("dummy_2", "passlib.tests.test_context") + + cc = LazyCryptContext(iter(["dummy_2", "des_crypt"]), deprecated=["des_crypt"]) + + self.assertFalse(has_crypt_handler("dummy_2", True)) + + self.assertTrue(cc.policy.handler_is_deprecated("des_crypt")) + self.assertEqual(cc.policy.schemes(), ["dummy_2", "des_crypt"]) + + self.assertTrue(has_crypt_handler("dummy_2", True)) + + def test_callable_constructor(self): + """test create_policy() hook, returning CryptPolicy""" + self.assertFalse(has_crypt_handler("dummy_2")) + register_crypt_handler_path("dummy_2", "passlib.tests.test_context") + + def create_policy(flag=False): + self.assertTrue(flag) + return CryptPolicy(schemes=iter(["dummy_2", "des_crypt"]), deprecated=["des_crypt"]) + + cc = LazyCryptContext(create_policy=create_policy, flag=True) + + self.assertFalse(has_crypt_handler("dummy_2", True)) + + self.assertTrue(cc.policy.handler_is_deprecated("des_crypt")) + self.assertEqual(cc.policy.schemes(), ["dummy_2", "des_crypt"]) + + self.assertTrue(has_crypt_handler("dummy_2", True)) + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/test_crypto_builtin_md4.py b/ansible/lib/python3.11/site-packages/passlib/tests/test_crypto_builtin_md4.py new file mode 100644 index 000000000..0aca1eb03 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/tests/test_crypto_builtin_md4.py @@ -0,0 +1,160 @@ +"""passlib.tests -- unittests for passlib.crypto._md4""" +#============================================================================= +# imports +#============================================================================= +from __future__ import with_statement, division +# core +from binascii import hexlify +import hashlib +# site +# pkg +# module +from passlib.utils.compat import bascii_to_str, PY3, u +from passlib.crypto.digest import lookup_hash +from passlib.tests.utils import TestCase, skipUnless +# local +__all__ = [ + "_Common_MD4_Test", + "MD4_Builtin_Test", + "MD4_SSL_Test", +] +#============================================================================= +# test pure-python MD4 implementation +#============================================================================= +class _Common_MD4_Test(TestCase): + """common code for testing md4 backends""" + + vectors = [ + # input -> hex digest + # test vectors from http://www.faqs.org/rfcs/rfc1320.html - A.5 + (b"", "31d6cfe0d16ae931b73c59d7e0c089c0"), + (b"a", "bde52cb31de33e46245e05fbdbd6fb24"), + (b"abc", "a448017aaf21d8525fc10ae87aa6729d"), + (b"message digest", "d9130a8164549fe818874806e1c7014b"), + (b"abcdefghijklmnopqrstuvwxyz", "d79e1c308aa5bbcdeea8ed63df412da9"), + (b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", "043f8582f241db351ce627e153e7f0e4"), + (b"12345678901234567890123456789012345678901234567890123456789012345678901234567890", "e33b4ddc9c38f2199c3e7b164fcc0536"), + ] + + def get_md4_const(self): + """ + get md4 constructor -- + overridden by subclasses to use alternate backends. + """ + return lookup_hash("md4").const + + def test_attrs(self): + """informational attributes""" + h = self.get_md4_const()() + self.assertEqual(h.name, "md4") + self.assertEqual(h.digest_size, 16) + self.assertEqual(h.block_size, 64) + + def test_md4_update(self): + """update() method""" + md4 = self.get_md4_const() + h = md4(b'') + self.assertEqual(h.hexdigest(), "31d6cfe0d16ae931b73c59d7e0c089c0") + + h.update(b'a') + self.assertEqual(h.hexdigest(), "bde52cb31de33e46245e05fbdbd6fb24") + + h.update(b'bcdefghijklmnopqrstuvwxyz') + self.assertEqual(h.hexdigest(), "d79e1c308aa5bbcdeea8ed63df412da9") + + if PY3: + # reject unicode, hash should return digest of b'' + h = md4() + self.assertRaises(TypeError, h.update, u('a')) + self.assertEqual(h.hexdigest(), "31d6cfe0d16ae931b73c59d7e0c089c0") + else: + # coerce unicode to ascii, hash should return digest of b'a' + h = md4() + h.update(u('a')) + self.assertEqual(h.hexdigest(), "bde52cb31de33e46245e05fbdbd6fb24") + + def test_md4_hexdigest(self): + """hexdigest() method""" + md4 = self.get_md4_const() + for input, hex in self.vectors: + out = md4(input).hexdigest() + self.assertEqual(out, hex) + + def test_md4_digest(self): + """digest() method""" + md4 = self.get_md4_const() + for input, hex in self.vectors: + out = bascii_to_str(hexlify(md4(input).digest())) + self.assertEqual(out, hex) + + def test_md4_copy(self): + """copy() method""" + md4 = self.get_md4_const() + h = md4(b'abc') + + h2 = h.copy() + h2.update(b'def') + self.assertEqual(h2.hexdigest(), '804e7f1c2586e50b49ac65db5b645131') + + h.update(b'ghi') + self.assertEqual(h.hexdigest(), 'c5225580bfe176f6deeee33dee98732c') + + +#------------------------------------------------------------------------ +# create subclasses to test various backends +#------------------------------------------------------------------------ + +def has_native_md4(): # pragma: no cover -- runtime detection + """ + check if hashlib natively supports md4. + """ + try: + hashlib.new("md4") + return True + except ValueError: + # not supported - ssl probably missing (e.g. ironpython) + return False + + +@skipUnless(has_native_md4(), "hashlib lacks ssl/md4 support") +class MD4_SSL_Test(_Common_MD4_Test): + descriptionPrefix = "hashlib.new('md4')" + + # NOTE: we trust ssl got md4 implementation right, + # this is more to test our test is correct :) + + def setUp(self): + super(MD4_SSL_Test, self).setUp() + + # make sure we're using right constructor. + self.assertEqual(self.get_md4_const().__module__, "hashlib") + + +class MD4_Builtin_Test(_Common_MD4_Test): + descriptionPrefix = "passlib.crypto._md4.md4()" + + def setUp(self): + super(MD4_Builtin_Test, self).setUp() + + if has_native_md4(): + + # Temporarily make lookup_hash() use builtin pure-python implementation, + # by monkeypatching hashlib.new() to ensure we fall back to passlib's md4 class. + orig = hashlib.new + def wrapper(name, *args): + if name == "md4": + raise ValueError("md4 disabled for testing") + return orig(name, *args) + self.patchAttr(hashlib, "new", wrapper) + + # flush cache before & after test, since we're mucking with it. + lookup_hash.clear_cache() + self.addCleanup(lookup_hash.clear_cache) + + # make sure we're using right constructor. + self.assertEqual(self.get_md4_const().__module__, "passlib.crypto._md4") + + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/test_crypto_des.py b/ansible/lib/python3.11/site-packages/passlib/tests/test_crypto_des.py new file mode 100644 index 000000000..ab31845ec --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/tests/test_crypto_des.py @@ -0,0 +1,194 @@ +"""passlib.tests -- unittests for passlib.crypto.des""" +#============================================================================= +# imports +#============================================================================= +from __future__ import with_statement, division +# core +from functools import partial +# site +# pkg +# module +from passlib.utils import getrandbytes +from passlib.tests.utils import TestCase + +#============================================================================= +# test DES routines +#============================================================================= +class DesTest(TestCase): + descriptionPrefix = "passlib.crypto.des" + + # test vectors taken from http://www.skepticfiles.org/faq/testdes.htm + des_test_vectors = [ + # key, plaintext, ciphertext + (0x0000000000000000, 0x0000000000000000, 0x8CA64DE9C1B123A7), + (0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0x7359B2163E4EDC58), + (0x3000000000000000, 0x1000000000000001, 0x958E6E627A05557B), + (0x1111111111111111, 0x1111111111111111, 0xF40379AB9E0EC533), + (0x0123456789ABCDEF, 0x1111111111111111, 0x17668DFC7292532D), + (0x1111111111111111, 0x0123456789ABCDEF, 0x8A5AE1F81AB8F2DD), + (0x0000000000000000, 0x0000000000000000, 0x8CA64DE9C1B123A7), + (0xFEDCBA9876543210, 0x0123456789ABCDEF, 0xED39D950FA74BCC4), + (0x7CA110454A1A6E57, 0x01A1D6D039776742, 0x690F5B0D9A26939B), + (0x0131D9619DC1376E, 0x5CD54CA83DEF57DA, 0x7A389D10354BD271), + (0x07A1133E4A0B2686, 0x0248D43806F67172, 0x868EBB51CAB4599A), + (0x3849674C2602319E, 0x51454B582DDF440A, 0x7178876E01F19B2A), + (0x04B915BA43FEB5B6, 0x42FD443059577FA2, 0xAF37FB421F8C4095), + (0x0113B970FD34F2CE, 0x059B5E0851CF143A, 0x86A560F10EC6D85B), + (0x0170F175468FB5E6, 0x0756D8E0774761D2, 0x0CD3DA020021DC09), + (0x43297FAD38E373FE, 0x762514B829BF486A, 0xEA676B2CB7DB2B7A), + (0x07A7137045DA2A16, 0x3BDD119049372802, 0xDFD64A815CAF1A0F), + (0x04689104C2FD3B2F, 0x26955F6835AF609A, 0x5C513C9C4886C088), + (0x37D06BB516CB7546, 0x164D5E404F275232, 0x0A2AEEAE3FF4AB77), + (0x1F08260D1AC2465E, 0x6B056E18759F5CCA, 0xEF1BF03E5DFA575A), + (0x584023641ABA6176, 0x004BD6EF09176062, 0x88BF0DB6D70DEE56), + (0x025816164629B007, 0x480D39006EE762F2, 0xA1F9915541020B56), + (0x49793EBC79B3258F, 0x437540C8698F3CFA, 0x6FBF1CAFCFFD0556), + (0x4FB05E1515AB73A7, 0x072D43A077075292, 0x2F22E49BAB7CA1AC), + (0x49E95D6D4CA229BF, 0x02FE55778117F12A, 0x5A6B612CC26CCE4A), + (0x018310DC409B26D6, 0x1D9D5C5018F728C2, 0x5F4C038ED12B2E41), + (0x1C587F1C13924FEF, 0x305532286D6F295A, 0x63FAC0D034D9F793), + (0x0101010101010101, 0x0123456789ABCDEF, 0x617B3A0CE8F07100), + (0x1F1F1F1F0E0E0E0E, 0x0123456789ABCDEF, 0xDB958605F8C8C606), + (0xE0FEE0FEF1FEF1FE, 0x0123456789ABCDEF, 0xEDBFD1C66C29CCC7), + (0x0000000000000000, 0xFFFFFFFFFFFFFFFF, 0x355550B2150E2451), + (0xFFFFFFFFFFFFFFFF, 0x0000000000000000, 0xCAAAAF4DEAF1DBAE), + (0x0123456789ABCDEF, 0x0000000000000000, 0xD5D44FF720683D0D), + (0xFEDCBA9876543210, 0xFFFFFFFFFFFFFFFF, 0x2A2BB008DF97C2F2), + ] + + def test_01_expand(self): + """expand_des_key()""" + from passlib.crypto.des import expand_des_key, shrink_des_key, \ + _KDATA_MASK, INT_56_MASK + + # make sure test vectors are preserved (sans parity bits) + # uses ints, bytes are tested under # 02 + for key1, _, _ in self.des_test_vectors: + key2 = shrink_des_key(key1) + key3 = expand_des_key(key2) + # NOTE: this assumes expand_des_key() sets parity bits to 0 + self.assertEqual(key3, key1 & _KDATA_MASK) + + # type checks + self.assertRaises(TypeError, expand_des_key, 1.0) + + # too large + self.assertRaises(ValueError, expand_des_key, INT_56_MASK+1) + self.assertRaises(ValueError, expand_des_key, b"\x00"*8) + + # too small + self.assertRaises(ValueError, expand_des_key, -1) + self.assertRaises(ValueError, expand_des_key, b"\x00"*6) + + def test_02_shrink(self): + """shrink_des_key()""" + from passlib.crypto.des import expand_des_key, shrink_des_key, INT_64_MASK + rng = self.getRandom() + + # make sure reverse works for some random keys + # uses bytes, ints are tested under # 01 + for i in range(20): + key1 = getrandbytes(rng, 7) + key2 = expand_des_key(key1) + key3 = shrink_des_key(key2) + self.assertEqual(key3, key1) + + # type checks + self.assertRaises(TypeError, shrink_des_key, 1.0) + + # too large + self.assertRaises(ValueError, shrink_des_key, INT_64_MASK+1) + self.assertRaises(ValueError, shrink_des_key, b"\x00"*9) + + # too small + self.assertRaises(ValueError, shrink_des_key, -1) + self.assertRaises(ValueError, shrink_des_key, b"\x00"*7) + + def _random_parity(self, key): + """randomize parity bits""" + from passlib.crypto.des import _KDATA_MASK, _KPARITY_MASK, INT_64_MASK + rng = self.getRandom() + return (key & _KDATA_MASK) | (rng.randint(0,INT_64_MASK) & _KPARITY_MASK) + + def test_03_encrypt_bytes(self): + """des_encrypt_block()""" + from passlib.crypto.des import (des_encrypt_block, shrink_des_key, + _pack64, _unpack64) + + # run through test vectors + for key, plaintext, correct in self.des_test_vectors: + # convert to bytes + key = _pack64(key) + plaintext = _pack64(plaintext) + correct = _pack64(correct) + + # test 64-bit key + result = des_encrypt_block(key, plaintext) + self.assertEqual(result, correct, "key=%r plaintext=%r:" % + (key, plaintext)) + + # test 56-bit version + key2 = shrink_des_key(key) + result = des_encrypt_block(key2, plaintext) + self.assertEqual(result, correct, "key=%r shrink(key)=%r plaintext=%r:" % + (key, key2, plaintext)) + + # test with random parity bits + for _ in range(20): + key3 = _pack64(self._random_parity(_unpack64(key))) + result = des_encrypt_block(key3, plaintext) + self.assertEqual(result, correct, "key=%r rndparity(key)=%r plaintext=%r:" % + (key, key3, plaintext)) + + # check invalid keys + stub = b'\x00' * 8 + self.assertRaises(TypeError, des_encrypt_block, 0, stub) + self.assertRaises(ValueError, des_encrypt_block, b'\x00'*6, stub) + + # check invalid input + self.assertRaises(TypeError, des_encrypt_block, stub, 0) + self.assertRaises(ValueError, des_encrypt_block, stub, b'\x00'*7) + + # check invalid salts + self.assertRaises(ValueError, des_encrypt_block, stub, stub, salt=-1) + self.assertRaises(ValueError, des_encrypt_block, stub, stub, salt=1<<24) + + # check invalid rounds + self.assertRaises(ValueError, des_encrypt_block, stub, stub, 0, rounds=0) + + def test_04_encrypt_ints(self): + """des_encrypt_int_block()""" + from passlib.crypto.des import des_encrypt_int_block + + # run through test vectors + for key, plaintext, correct in self.des_test_vectors: + # test 64-bit key + result = des_encrypt_int_block(key, plaintext) + self.assertEqual(result, correct, "key=%r plaintext=%r:" % + (key, plaintext)) + + # test with random parity bits + for _ in range(20): + key3 = self._random_parity(key) + result = des_encrypt_int_block(key3, plaintext) + self.assertEqual(result, correct, "key=%r rndparity(key)=%r plaintext=%r:" % + (key, key3, plaintext)) + + # check invalid keys + self.assertRaises(TypeError, des_encrypt_int_block, b'\x00', 0) + self.assertRaises(ValueError, des_encrypt_int_block, -1, 0) + + # check invalid input + self.assertRaises(TypeError, des_encrypt_int_block, 0, b'\x00') + self.assertRaises(ValueError, des_encrypt_int_block, 0, -1) + + # check invalid salts + self.assertRaises(ValueError, des_encrypt_int_block, 0, 0, salt=-1) + self.assertRaises(ValueError, des_encrypt_int_block, 0, 0, salt=1<<24) + + # check invalid rounds + self.assertRaises(ValueError, des_encrypt_int_block, 0, 0, 0, rounds=0) + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/test_crypto_digest.py b/ansible/lib/python3.11/site-packages/passlib/tests/test_crypto_digest.py new file mode 100644 index 000000000..461d20965 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/tests/test_crypto_digest.py @@ -0,0 +1,544 @@ +"""tests for passlib.utils.(des|pbkdf2|md4)""" +#============================================================================= +# imports +#============================================================================= +from __future__ import with_statement, division +# core +from binascii import hexlify +import hashlib +import warnings +# site +# pkg +# module +from passlib.exc import UnknownHashError +from passlib.utils.compat import PY3, u, JYTHON +from passlib.tests.utils import TestCase, TEST_MODE, skipUnless, hb + +#============================================================================= +# test assorted crypto helpers +#============================================================================= +class HashInfoTest(TestCase): + """test various crypto functions""" + descriptionPrefix = "passlib.crypto.digest" + + #: list of formats norm_hash_name() should support + norm_hash_formats = ["hashlib", "iana"] + + #: test cases for norm_hash_name() + #: each row contains (iana name, hashlib name, ... 0+ unnormalized names) + norm_hash_samples = [ + # real hashes + ("md5", "md5", "SCRAM-MD5-PLUS", "MD-5"), + ("sha1", "sha-1", "SCRAM-SHA-1", "SHA1"), + ("sha256", "sha-256", "SHA_256", "sha2-256"), + ("ripemd160", "ripemd-160", "SCRAM-RIPEMD-160", "RIPEmd160", + # NOTE: there was an older "RIPEMD" & "RIPEMD-128", but python treates "RIPEMD" + # as alias for "RIPEMD-160" + "ripemd", "SCRAM-RIPEMD"), + + # fake hashes (to check if fallback normalization behaves sanely) + ("sha4_256", "sha4-256", "SHA4-256", "SHA-4-256"), + ("test128", "test-128", "TEST128"), + ("test2", "test2", "TEST-2"), + ("test3_128", "test3-128", "TEST-3-128"), + ] + + def test_norm_hash_name(self): + """norm_hash_name()""" + from itertools import chain + from passlib.crypto.digest import norm_hash_name, _known_hash_names + + # snapshot warning state, ignore unknown hash warnings + ctx = warnings.catch_warnings() + ctx.__enter__() + self.addCleanup(ctx.__exit__) + warnings.filterwarnings("ignore", '.*unknown hash') + warnings.filterwarnings("ignore", '.*unsupported hash') + + # test string types + self.assertEqual(norm_hash_name(u("MD4")), "md4") + self.assertEqual(norm_hash_name(b"MD4"), "md4") + self.assertRaises(TypeError, norm_hash_name, None) + + # test selected results + for row in chain(_known_hash_names, self.norm_hash_samples): + for idx, format in enumerate(self.norm_hash_formats): + correct = row[idx] + for value in row: + result = norm_hash_name(value, format) + self.assertEqual(result, correct, + "name=%r, format=%r:" % (value, + format)) + + def test_lookup_hash_ctor(self): + """lookup_hash() -- constructor""" + from passlib.crypto.digest import lookup_hash + + # invalid/unknown names should be rejected + self.assertRaises(ValueError, lookup_hash, "new") + self.assertRaises(ValueError, lookup_hash, "__name__") + self.assertRaises(ValueError, lookup_hash, "sha4") + + # 1. should return hashlib builtin if found + self.assertEqual(lookup_hash("md5"), (hashlib.md5, 16, 64)) + + # 2. should return wrapper around hashlib.new() if found + try: + hashlib.new("sha") + has_sha = True + except ValueError: + has_sha = False + if has_sha: + record = lookup_hash("sha") + const = record[0] + self.assertEqual(record, (const, 20, 64)) + self.assertEqual(hexlify(const(b"abc").digest()), + b"0164b8a914cd2a5e74c4f7ff082c4d97f1edf880") + + else: + self.assertRaises(ValueError, lookup_hash, "sha") + + # 3. should fall back to builtin md4 + try: + hashlib.new("md4") + has_md4 = True + except ValueError: + has_md4 = False + record = lookup_hash("md4") + const = record[0] + if not has_md4: + from passlib.crypto._md4 import md4 + self.assertIs(const, md4) + self.assertEqual(record, (const, 16, 64)) + self.assertEqual(hexlify(const(b"abc").digest()), + b"a448017aaf21d8525fc10ae87aa6729d") + + # should memoize records + self.assertIs(lookup_hash("md5"), lookup_hash("md5")) + + def test_lookup_hash_w_unknown_name(self): + """lookup_hash() -- unknown hash name""" + from passlib.crypto.digest import lookup_hash + + # unknown names should be rejected by default + self.assertRaises(UnknownHashError, lookup_hash, "xxx256") + + # required=False should return stub record instead + info = lookup_hash("xxx256", required=False) + self.assertFalse(info.supported) + self.assertRaisesRegex(UnknownHashError, "unknown hash: 'xxx256'", info.const) + self.assertEqual(info.name, "xxx256") + self.assertEqual(info.digest_size, None) + self.assertEqual(info.block_size, None) + + # should cache stub records + info2 = lookup_hash("xxx256", required=False) + self.assertIs(info2, info) + + def test_mock_fips_mode(self): + """ + lookup_hash() -- test set_mock_fips_mode() + """ + from passlib.crypto.digest import lookup_hash, _set_mock_fips_mode + + # check if md5 is available so we can test mock helper + if not lookup_hash("md5", required=False).supported: + raise self.skipTest("md5 not supported") + + # enable monkeypatch to mock up fips mode + _set_mock_fips_mode() + self.addCleanup(_set_mock_fips_mode, False) + + pat = "'md5' hash disabled for fips" + self.assertRaisesRegex(UnknownHashError, pat, lookup_hash, "md5") + + info = lookup_hash("md5", required=False) + self.assertRegex(info.error_text, pat) + self.assertRaisesRegex(UnknownHashError, pat, info.const) + + # should use hardcoded fallback info + self.assertEqual(info.digest_size, 16) + self.assertEqual(info.block_size, 64) + + def test_lookup_hash_metadata(self): + """lookup_hash() -- metadata""" + + from passlib.crypto.digest import lookup_hash + + # quick test of metadata using known reference - sha256 + info = lookup_hash("sha256") + self.assertEqual(info.name, "sha256") + self.assertEqual(info.iana_name, "sha-256") + self.assertEqual(info.block_size, 64) + self.assertEqual(info.digest_size, 32) + self.assertIs(lookup_hash("SHA2-256"), info) + + # quick test of metadata using known reference - md5 + info = lookup_hash("md5") + self.assertEqual(info.name, "md5") + self.assertEqual(info.iana_name, "md5") + self.assertEqual(info.block_size, 64) + self.assertEqual(info.digest_size, 16) + + def test_lookup_hash_alt_types(self): + """lookup_hash() -- alternate types""" + + from passlib.crypto.digest import lookup_hash + + info = lookup_hash("sha256") + self.assertIs(lookup_hash(info), info) + self.assertIs(lookup_hash(info.const), info) + + self.assertRaises(TypeError, lookup_hash, 123) + + # TODO: write full test of compile_hmac() -- currently relying on pbkdf2_hmac() tests + +#============================================================================= +# test PBKDF1 support +#============================================================================= +class Pbkdf1_Test(TestCase): + """test kdf helpers""" + descriptionPrefix = "passlib.crypto.digest.pbkdf1" + + pbkdf1_tests = [ + # (password, salt, rounds, keylen, hash, result) + + # + # from http://www.di-mgt.com.au/cryptoKDFs.html + # + (b'password', hb('78578E5A5D63CB06'), 1000, 16, 'sha1', hb('dc19847e05c64d2faf10ebfb4a3d2a20')), + + # + # custom + # + (b'password', b'salt', 1000, 0, 'md5', b''), + (b'password', b'salt', 1000, 1, 'md5', hb('84')), + (b'password', b'salt', 1000, 8, 'md5', hb('8475c6a8531a5d27')), + (b'password', b'salt', 1000, 16, 'md5', hb('8475c6a8531a5d27e386cd496457812c')), + (b'password', b'salt', 1000, None, 'md5', hb('8475c6a8531a5d27e386cd496457812c')), + (b'password', b'salt', 1000, None, 'sha1', hb('4a8fd48e426ed081b535be5769892fa396293efb')), + ] + if not JYTHON: # FIXME: find out why not jython, or reenable this. + pbkdf1_tests.append( + (b'password', b'salt', 1000, None, 'md4', hb('f7f2e91100a8f96190f2dd177cb26453')) + ) + + def test_known(self): + """test reference vectors""" + from passlib.crypto.digest import pbkdf1 + for secret, salt, rounds, keylen, digest, correct in self.pbkdf1_tests: + result = pbkdf1(digest, secret, salt, rounds, keylen) + self.assertEqual(result, correct) + + def test_border(self): + """test border cases""" + from passlib.crypto.digest import pbkdf1 + def helper(secret=b'secret', salt=b'salt', rounds=1, keylen=1, hash='md5'): + return pbkdf1(hash, secret, salt, rounds, keylen) + helper() + + # salt/secret wrong type + self.assertRaises(TypeError, helper, secret=1) + self.assertRaises(TypeError, helper, salt=1) + + # non-existent hashes + self.assertRaises(ValueError, helper, hash='missing') + + # rounds < 1 and wrong type + self.assertRaises(ValueError, helper, rounds=0) + self.assertRaises(TypeError, helper, rounds='1') + + # keylen < 0, keylen > block_size, and wrong type + self.assertRaises(ValueError, helper, keylen=-1) + self.assertRaises(ValueError, helper, keylen=17, hash='md5') + self.assertRaises(TypeError, helper, keylen='1') + +#============================================================================= +# test PBKDF2-HMAC support +#============================================================================= + +# import the test subject +from passlib.crypto.digest import pbkdf2_hmac, PBKDF2_BACKENDS + +# NOTE: relying on tox to verify this works under all the various backends. +class Pbkdf2Test(TestCase): + """test pbkdf2() support""" + descriptionPrefix = "passlib.crypto.digest.pbkdf2_hmac() " % ", ".join(PBKDF2_BACKENDS) + + pbkdf2_test_vectors = [ + # (result, secret, salt, rounds, keylen, digest="sha1") + + # + # from rfc 3962 + # + + # test case 1 / 128 bit + ( + hb("cdedb5281bb2f801565a1122b2563515"), + b"password", b"ATHENA.MIT.EDUraeburn", 1, 16 + ), + + # test case 2 / 128 bit + ( + hb("01dbee7f4a9e243e988b62c73cda935d"), + b"password", b"ATHENA.MIT.EDUraeburn", 2, 16 + ), + + # test case 2 / 256 bit + ( + hb("01dbee7f4a9e243e988b62c73cda935da05378b93244ec8f48a99e61ad799d86"), + b"password", b"ATHENA.MIT.EDUraeburn", 2, 32 + ), + + # test case 3 / 256 bit + ( + hb("5c08eb61fdf71e4e4ec3cf6ba1f5512ba7e52ddbc5e5142f708a31e2e62b1e13"), + b"password", b"ATHENA.MIT.EDUraeburn", 1200, 32 + ), + + # test case 4 / 256 bit + ( + hb("d1daa78615f287e6a1c8b120d7062a493f98d203e6be49a6adf4fa574b6e64ee"), + b"password", b'\x12\x34\x56\x78\x78\x56\x34\x12', 5, 32 + ), + + # test case 5 / 256 bit + ( + hb("139c30c0966bc32ba55fdbf212530ac9c5ec59f1a452f5cc9ad940fea0598ed1"), + b"X"*64, b"pass phrase equals block size", 1200, 32 + ), + + # test case 6 / 256 bit + ( + hb("9ccad6d468770cd51b10e6a68721be611a8b4d282601db3b36be9246915ec82a"), + b"X"*65, b"pass phrase exceeds block size", 1200, 32 + ), + + # + # from rfc 6070 + # + ( + hb("0c60c80f961f0e71f3a9b524af6012062fe037a6"), + b"password", b"salt", 1, 20, + ), + + ( + hb("ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957"), + b"password", b"salt", 2, 20, + ), + + ( + hb("4b007901b765489abead49d926f721d065a429c1"), + b"password", b"salt", 4096, 20, + ), + + # just runs too long - could enable if ALL option is set + ##( + ## + ## hb("eefe3d61cd4da4e4e9945b3d6ba2158c2634e984"), + ## "password", "salt", 16777216, 20, + ##), + + ( + hb("3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038"), + b"passwordPASSWORDpassword", + b"saltSALTsaltSALTsaltSALTsaltSALTsalt", + 4096, 25, + ), + + ( + hb("56fa6aa75548099dcc37d7f03425e0c3"), + b"pass\00word", b"sa\00lt", 4096, 16, + ), + + # + # from example in http://grub.enbug.org/Authentication + # + ( + hb("887CFF169EA8335235D8004242AA7D6187A41E3187DF0CE14E256D85ED" + "97A97357AAA8FF0A3871AB9EEFF458392F462F495487387F685B7472FC" + "6C29E293F0A0"), + b"hello", + hb("9290F727ED06C38BA4549EF7DE25CF5642659211B7FC076F2D28FEFD71" + "784BB8D8F6FB244A8CC5C06240631B97008565A120764C0EE9C2CB0073" + "994D79080136"), + 10000, 64, "sha512" + ), + + # + # test vectors from fastpbkdf2 + # + ( + hb('55ac046e56e3089fec1691c22544b605f94185216dde0465e68b9d57c20dacbc' + '49ca9cccf179b645991664b39d77ef317c71b845b1e30bd509112041d3a19783'), + b'passwd', b'salt', 1, 64, 'sha256', + ), + + ( + hb('4ddcd8f60b98be21830cee5ef22701f9641a4418d04c0414aeff08876b34ab56' + 'a1d425a1225833549adb841b51c9b3176a272bdebba1d078478f62b397f33c8d'), + b'Password', b'NaCl', 80000, 64, 'sha256', + ), + + ( + hb('120fb6cffcf8b32c43e7225256c4f837a86548c92ccc35480805987cb70be17b'), + b'password', b'salt', 1, 32, 'sha256', + ), + + ( + hb('ae4d0c95af6b46d32d0adff928f06dd02a303f8ef3c251dfd6e2d85a95474c43'), + b'password', b'salt', 2, 32, 'sha256', + ), + + ( + hb('c5e478d59288c841aa530db6845c4c8d962893a001ce4e11a4963873aa98134a'), + b'password', b'salt', 4096, 32, 'sha256', + ), + + ( + hb('348c89dbcbd32b2f32d814b8116e84cf2b17347ebc1800181c4e2a1fb8dd53e1c' + '635518c7dac47e9'), + b'passwordPASSWORDpassword', b'saltSALTsaltSALTsaltSALTsaltSALTsalt', + 4096, 40, 'sha256', + ), + + ( + hb('9e83f279c040f2a11aa4a02b24c418f2d3cb39560c9627fa4f47e3bcc2897c3d'), + b'', b'salt', 1024, 32, 'sha256', + ), + + ( + hb('ea5808411eb0c7e830deab55096cee582761e22a9bc034e3ece925225b07bf46'), + b'password', b'', 1024, 32, 'sha256', + ), + + ( + hb('89b69d0516f829893c696226650a8687'), + b'pass\x00word', b'sa\x00lt', 4096, 16, 'sha256', + ), + + ( + hb('867f70cf1ade02cff3752599a3a53dc4af34c7a669815ae5d513554e1c8cf252'), + b'password', b'salt', 1, 32, 'sha512', + ), + + ( + hb('e1d9c16aa681708a45f5c7c4e215ceb66e011a2e9f0040713f18aefdb866d53c'), + b'password', b'salt', 2, 32, 'sha512', + ), + + ( + hb('d197b1b33db0143e018b12f3d1d1479e6cdebdcc97c5c0f87f6902e072f457b5'), + b'password', b'salt', 4096, 32, 'sha512', + ), + + ( + hb('6e23f27638084b0f7ea1734e0d9841f55dd29ea60a834466f3396bac801fac1eeb' + '63802f03a0b4acd7603e3699c8b74437be83ff01ad7f55dac1ef60f4d56480c35e' + 'e68fd52c6936'), + b'passwordPASSWORDpassword', b'saltSALTsaltSALTsaltSALTsaltSALTsalt', + 1, 72, 'sha512', + ), + + ( + hb('0c60c80f961f0e71f3a9b524af6012062fe037a6'), + b'password', b'salt', 1, 20, 'sha1', + ), + + # + # custom tests + # + ( + hb('e248fb6b13365146f8ac6307cc222812'), + b"secret", b"salt", 10, 16, "sha1", + ), + ( + hb('e248fb6b13365146f8ac6307cc2228127872da6d'), + b"secret", b"salt", 10, None, "sha1", + ), + ( + hb('b1d5485772e6f76d5ebdc11b38d3eff0a5b2bd50dc11f937e86ecacd0cd40d1b' + '9113e0734e3b76a3'), + b"secret", b"salt", 62, 40, "md5", + ), + ( + hb('ea014cc01f78d3883cac364bb5d054e2be238fb0b6081795a9d84512126e3129' + '062104d2183464c4'), + b"secret", b"salt", 62, 40, "md4", + ), + ] + + def test_known(self): + """test reference vectors""" + for row in self.pbkdf2_test_vectors: + correct, secret, salt, rounds, keylen = row[:5] + digest = row[5] if len(row) == 6 else "sha1" + result = pbkdf2_hmac(digest, secret, salt, rounds, keylen) + self.assertEqual(result, correct) + + def test_backends(self): + """verify expected backends are present""" + from passlib.crypto.digest import PBKDF2_BACKENDS + + # check for fastpbkdf2 + try: + import fastpbkdf2 + has_fastpbkdf2 = True + except ImportError: + has_fastpbkdf2 = False + self.assertEqual("fastpbkdf2" in PBKDF2_BACKENDS, has_fastpbkdf2) + + # check for hashlib + try: + from hashlib import pbkdf2_hmac + has_hashlib_ssl = pbkdf2_hmac.__module__ != "hashlib" + except ImportError: + has_hashlib_ssl = False + self.assertEqual("hashlib-ssl" in PBKDF2_BACKENDS, has_hashlib_ssl) + + # check for appropriate builtin + from passlib.utils.compat import PY3 + if PY3: + self.assertIn("builtin-from-bytes", PBKDF2_BACKENDS) + else: + # XXX: only true as long as this is preferred over hexlify + self.assertIn("builtin-unpack", PBKDF2_BACKENDS) + + def test_border(self): + """test border cases""" + def helper(secret=b'password', salt=b'salt', rounds=1, keylen=None, digest="sha1"): + return pbkdf2_hmac(digest, secret, salt, rounds, keylen) + helper() + + # invalid rounds + self.assertRaises(ValueError, helper, rounds=-1) + self.assertRaises(ValueError, helper, rounds=0) + self.assertRaises(TypeError, helper, rounds='x') + + # invalid keylen + helper(keylen=1) + self.assertRaises(ValueError, helper, keylen=-1) + self.assertRaises(ValueError, helper, keylen=0) + # NOTE: hashlib actually throws error for keylen>=MAX_SINT32, + # but pbkdf2 forbids anything > MAX_UINT32 * digest_size + self.assertRaises(OverflowError, helper, keylen=20*(2**32-1)+1) + self.assertRaises(TypeError, helper, keylen='x') + + # invalid secret/salt type + self.assertRaises(TypeError, helper, salt=5) + self.assertRaises(TypeError, helper, secret=5) + + # invalid hash + self.assertRaises(ValueError, helper, digest='foo') + self.assertRaises(TypeError, helper, digest=5) + + def test_default_keylen(self): + """test keylen==None""" + def helper(secret=b'password', salt=b'salt', rounds=1, keylen=None, digest="sha1"): + return pbkdf2_hmac(digest, secret, salt, rounds, keylen) + self.assertEqual(len(helper(digest='sha1')), 20) + self.assertEqual(len(helper(digest='sha256')), 32) + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/test_crypto_scrypt.py b/ansible/lib/python3.11/site-packages/passlib/tests/test_crypto_scrypt.py new file mode 100644 index 000000000..73ff1fa0d --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/tests/test_crypto_scrypt.py @@ -0,0 +1,634 @@ +"""tests for passlib.utils.scrypt""" +#============================================================================= +# imports +#============================================================================= +# core +from binascii import hexlify +import hashlib +import logging; log = logging.getLogger(__name__) +import struct +import warnings +warnings.filterwarnings("ignore", ".*using builtin scrypt backend.*") +# site +# pkg +from passlib import exc +from passlib.utils import getrandbytes +from passlib.utils.compat import PYPY, u, bascii_to_str +from passlib.utils.decor import classproperty +from passlib.tests.utils import TestCase, skipUnless, TEST_MODE, hb +# subject +from passlib.crypto import scrypt as scrypt_mod +# local +__all__ = [ + "ScryptEngineTest", + "BuiltinScryptTest", + "FastScryptTest", +] + +#============================================================================= +# support functions +#============================================================================= +def hexstr(data): + """return bytes as hex str""" + return bascii_to_str(hexlify(data)) + +def unpack_uint32_list(data, check_count=None): + """unpack bytes as list of uint32 values""" + count = len(data) // 4 + assert check_count is None or check_count == count + return struct.unpack("<%dI" % count, data) + +def seed_bytes(seed, count): + """ + generate random reference bytes from specified seed. + used to generate some predictable test vectors. + """ + if hasattr(seed, "encode"): + seed = seed.encode("ascii") + buf = b'' + i = 0 + while len(buf) < count: + buf += hashlib.sha256(seed + struct.pack("" % cls.backend + backend = None + + #============================================================================= + # setup + #============================================================================= + def setUp(self): + assert self.backend + scrypt_mod._set_backend(self.backend) + super(_CommonScryptTest, self).setUp() + + #============================================================================= + # reference vectors + #============================================================================= + + reference_vectors = [ + # entry format: (secret, salt, n, r, p, keylen, result) + + #------------------------------------------------------------------------ + # test vectors from scrypt whitepaper -- + # http://www.tarsnap.com/scrypt/scrypt.pdf, appendix b + # + # also present in (expired) scrypt rfc draft -- + # https://tools.ietf.org/html/draft-josefsson-scrypt-kdf-01, section 11 + #------------------------------------------------------------------------ + ("", "", 16, 1, 1, 64, hb(""" + 77 d6 57 62 38 65 7b 20 3b 19 ca 42 c1 8a 04 97 + f1 6b 48 44 e3 07 4a e8 df df fa 3f ed e2 14 42 + fc d0 06 9d ed 09 48 f8 32 6a 75 3a 0f c8 1f 17 + e8 d3 e0 fb 2e 0d 36 28 cf 35 e2 0c 38 d1 89 06 + """)), + + ("password", "NaCl", 1024, 8, 16, 64, hb(""" + fd ba be 1c 9d 34 72 00 78 56 e7 19 0d 01 e9 fe + 7c 6a d7 cb c8 23 78 30 e7 73 76 63 4b 37 31 62 + 2e af 30 d9 2e 22 a3 88 6f f1 09 27 9d 98 30 da + c7 27 af b9 4a 83 ee 6d 83 60 cb df a2 cc 06 40 + """)), + + # NOTE: the following are skipped for all backends unless TEST_MODE="full" + + ("pleaseletmein", "SodiumChloride", 16384, 8, 1, 64, hb(""" + 70 23 bd cb 3a fd 73 48 46 1c 06 cd 81 fd 38 eb + fd a8 fb ba 90 4f 8e 3e a9 b5 43 f6 54 5d a1 f2 + d5 43 29 55 61 3f 0f cf 62 d4 97 05 24 2a 9a f9 + e6 1e 85 dc 0d 65 1e 40 df cf 01 7b 45 57 58 87 + """)), + + # NOTE: the following are always skipped for the builtin backend, + # (just takes too long to be worth it) + + ("pleaseletmein", "SodiumChloride", 1048576, 8, 1, 64, hb(""" + 21 01 cb 9b 6a 51 1a ae ad db be 09 cf 70 f8 81 + ec 56 8d 57 4a 2f fd 4d ab e5 ee 98 20 ad aa 47 + 8e 56 fd 8f 4b a5 d0 9f fa 1c 6d 92 7c 40 f4 c3 + 37 30 40 49 e8 a9 52 fb cb f4 5c 6f a7 7a 41 a4 + """)), + ] + + def test_reference_vectors(self): + """reference vectors""" + for secret, salt, n, r, p, keylen, result in self.reference_vectors: + if n >= 1024 and TEST_MODE(max="default"): + # skip large values unless we're running full test suite + continue + if n > 16384 and self.backend == "builtin": + # skip largest vector for builtin, takes WAAY too long + # (46s under pypy, ~5m under cpython) + continue + log.debug("scrypt reference vector: %r %r n=%r r=%r p=%r", secret, salt, n, r, p) + self.assertEqual(scrypt_mod.scrypt(secret, salt, n, r, p, keylen), result) + + #============================================================================= + # fuzz testing + #============================================================================= + + _already_tested_others = None + + def test_other_backends(self): + """compare output to other backends""" + # only run once, since test is symetric. + # maybe this means it should go somewhere else? + if self._already_tested_others: + raise self.skipTest("already run under %r backend test" % self._already_tested_others) + self._already_tested_others = self.backend + rng = self.getRandom() + + # get available backends + orig = scrypt_mod.backend + available = set(name for name in scrypt_mod.backend_values + if scrypt_mod._has_backend(name)) + scrypt_mod._set_backend(orig) + available.discard(self.backend) + if not available: + raise self.skipTest("no other backends found") + + warnings.filterwarnings("ignore", "(?i)using builtin scrypt backend", + category=exc.PasslibSecurityWarning) + + # generate some random options, and cross-check output + for _ in range(10): + # NOTE: keeping values low due to builtin test + secret = getrandbytes(rng, rng.randint(0, 64)) + salt = getrandbytes(rng, rng.randint(0, 64)) + n = 1 << rng.randint(1, 10) + r = rng.randint(1, 8) + p = rng.randint(1, 3) + ks = rng.randint(1, 64) + previous = None + backends = set() + for name in available: + scrypt_mod._set_backend(name) + self.assertNotIn(scrypt_mod._scrypt, backends) + backends.add(scrypt_mod._scrypt) + result = hexstr(scrypt_mod.scrypt(secret, salt, n, r, p, ks)) + self.assertEqual(len(result), 2*ks) + if previous is not None: + self.assertEqual(result, previous, + msg="%r output differs from others %r: %r" % + (name, available, [secret, salt, n, r, p, ks])) + + #============================================================================= + # test input types + #============================================================================= + def test_backend(self): + """backend management""" + # clobber backend + scrypt_mod.backend = None + scrypt_mod._scrypt = None + self.assertRaises(TypeError, scrypt_mod.scrypt, 's', 's', 2, 2, 2, 16) + + # reload backend + scrypt_mod._set_backend(self.backend) + self.assertEqual(scrypt_mod.backend, self.backend) + scrypt_mod.scrypt('s', 's', 2, 2, 2, 16) + + # throw error for unknown backend + self.assertRaises(ValueError, scrypt_mod._set_backend, 'xxx') + self.assertEqual(scrypt_mod.backend, self.backend) + + def test_secret_param(self): + """'secret' parameter""" + + def run_scrypt(secret): + return hexstr(scrypt_mod.scrypt(secret, "salt", 2, 2, 2, 16)) + + # unicode + TEXT = u("abc\u00defg") + self.assertEqual(run_scrypt(TEXT), '05717106997bfe0da42cf4779a2f8bd8') + + # utf8 bytes + TEXT_UTF8 = b'abc\xc3\x9efg' + self.assertEqual(run_scrypt(TEXT_UTF8), '05717106997bfe0da42cf4779a2f8bd8') + + # latin1 bytes + TEXT_LATIN1 = b'abc\xdefg' + self.assertEqual(run_scrypt(TEXT_LATIN1), '770825d10eeaaeaf98e8a3c40f9f441d') + + # accept empty string + self.assertEqual(run_scrypt(""), 'ca1399e5fae5d3b9578dcd2b1faff6e2') + + # reject other types + self.assertRaises(TypeError, run_scrypt, None) + self.assertRaises(TypeError, run_scrypt, 1) + + def test_salt_param(self): + """'salt' parameter""" + + def run_scrypt(salt): + return hexstr(scrypt_mod.scrypt("secret", salt, 2, 2, 2, 16)) + + # unicode + TEXT = u("abc\u00defg") + self.assertEqual(run_scrypt(TEXT), 'a748ec0f4613929e9e5f03d1ab741d88') + + # utf8 bytes + TEXT_UTF8 = b'abc\xc3\x9efg' + self.assertEqual(run_scrypt(TEXT_UTF8), 'a748ec0f4613929e9e5f03d1ab741d88') + + # latin1 bytes + TEXT_LATIN1 = b'abc\xdefg' + self.assertEqual(run_scrypt(TEXT_LATIN1), '91d056fb76fb6e9a7d1cdfffc0a16cd1') + + # reject other types + self.assertRaises(TypeError, run_scrypt, None) + self.assertRaises(TypeError, run_scrypt, 1) + + def test_n_param(self): + """'n' (rounds) parameter""" + + def run_scrypt(n): + return hexstr(scrypt_mod.scrypt("secret", "salt", n, 2, 2, 16)) + + # must be > 1, and a power of 2 + self.assertRaises(ValueError, run_scrypt, -1) + self.assertRaises(ValueError, run_scrypt, 0) + self.assertRaises(ValueError, run_scrypt, 1) + self.assertEqual(run_scrypt(2), 'dacf2bca255e2870e6636fa8c8957a66') + self.assertRaises(ValueError, run_scrypt, 3) + self.assertRaises(ValueError, run_scrypt, 15) + self.assertEqual(run_scrypt(16), '0272b8fc72bc54b1159340ed99425233') + + def test_r_param(self): + """'r' (block size) parameter""" + def run_scrypt(r, n=2, p=2): + return hexstr(scrypt_mod.scrypt("secret", "salt", n, r, p, 16)) + + # must be > 1 + self.assertRaises(ValueError, run_scrypt, -1) + self.assertRaises(ValueError, run_scrypt, 0) + self.assertEqual(run_scrypt(1), '3d630447d9f065363b8a79b0b3670251') + self.assertEqual(run_scrypt(2), 'dacf2bca255e2870e6636fa8c8957a66') + self.assertEqual(run_scrypt(5), '114f05e985a903c27237b5578e763736') + + # reject r*p >= 2**30 + self.assertRaises(ValueError, run_scrypt, (1<<30), p=1) + self.assertRaises(ValueError, run_scrypt, (1<<30) / 2, p=2) + + def test_p_param(self): + """'p' (parallelism) parameter""" + def run_scrypt(p, n=2, r=2): + return hexstr(scrypt_mod.scrypt("secret", "salt", n, r, p, 16)) + + # must be > 1 + self.assertRaises(ValueError, run_scrypt, -1) + self.assertRaises(ValueError, run_scrypt, 0) + self.assertEqual(run_scrypt(1), 'f2960ea8b7d48231fcec1b89b784a6fa') + self.assertEqual(run_scrypt(2), 'dacf2bca255e2870e6636fa8c8957a66') + self.assertEqual(run_scrypt(5), '848a0eeb2b3543e7f543844d6ca79782') + + # reject r*p >= 2**30 + self.assertRaises(ValueError, run_scrypt, (1<<30), r=1) + self.assertRaises(ValueError, run_scrypt, (1<<30) / 2, r=2) + + def test_keylen_param(self): + """'keylen' parameter""" + rng = self.getRandom() + + def run_scrypt(keylen): + return hexstr(scrypt_mod.scrypt("secret", "salt", 2, 2, 2, keylen)) + + # must be > 0 + self.assertRaises(ValueError, run_scrypt, -1) + self.assertRaises(ValueError, run_scrypt, 0) + self.assertEqual(run_scrypt(1), 'da') + + # pick random value + ksize = rng.randint(1, 1 << 10) + self.assertEqual(len(run_scrypt(ksize)), 2*ksize) # 2 hex chars per output + + # one more than upper bound + self.assertRaises(ValueError, run_scrypt, ((2**32) - 1) * 32 + 1) + + #============================================================================= + # eoc + #============================================================================= + + +#----------------------------------------------------------------------- +# check what backends 'should' be available +#----------------------------------------------------------------------- + +def _can_import_cffi_scrypt(): + try: + import scrypt + except ImportError as err: + if "scrypt" in str(err): + return False + raise + return True + +has_cffi_scrypt = _can_import_cffi_scrypt() + + +def _can_import_stdlib_scrypt(): + try: + from hashlib import scrypt + return True + except ImportError: + return False + +has_stdlib_scrypt = _can_import_stdlib_scrypt() + +#----------------------------------------------------------------------- +# test individual backends +#----------------------------------------------------------------------- + +# NOTE: builtin version runs VERY slow (except under PyPy, where it's only 11x slower), +# so skipping under quick test mode. +@skipUnless(PYPY or TEST_MODE(min="default"), "skipped under current test mode") +class BuiltinScryptTest(_CommonScryptTest): + backend = "builtin" + + def setUp(self): + super(BuiltinScryptTest, self).setUp() + warnings.filterwarnings("ignore", "(?i)using builtin scrypt backend", + category=exc.PasslibSecurityWarning) + + def test_missing_backend(self): + """backend management -- missing backend""" + if has_stdlib_scrypt or has_cffi_scrypt: + raise self.skipTest("non-builtin backend is present") + self.assertRaises(exc.MissingBackendError, scrypt_mod._set_backend, 'scrypt') + + +@skipUnless(has_cffi_scrypt, "'scrypt' package not found") +class ScryptPackageTest(_CommonScryptTest): + backend = "scrypt" + + def test_default_backend(self): + """backend management -- default backend""" + if has_stdlib_scrypt: + raise self.skipTest("higher priority backend present") + scrypt_mod._set_backend("default") + self.assertEqual(scrypt_mod.backend, "scrypt") + + +@skipUnless(has_stdlib_scrypt, "'hashlib.scrypt()' not found") +class StdlibScryptTest(_CommonScryptTest): + backend = "stdlib" + + def test_default_backend(self): + """backend management -- default backend""" + scrypt_mod._set_backend("default") + self.assertEqual(scrypt_mod.backend, "stdlib") + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/test_ext_django.py b/ansible/lib/python3.11/site-packages/passlib/tests/test_ext_django.py new file mode 100644 index 000000000..2a0b418a8 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/tests/test_ext_django.py @@ -0,0 +1,1080 @@ +"""test passlib.ext.django""" +#============================================================================= +# imports +#============================================================================= +# core +from __future__ import absolute_import, division, print_function +import logging; log = logging.getLogger(__name__) +import sys +import re +# site +# pkg +from passlib import apps as _apps, exc, registry +from passlib.apps import django10_context, django14_context, django16_context +from passlib.context import CryptContext +from passlib.ext.django.utils import ( + DJANGO_VERSION, MIN_DJANGO_VERSION, DjangoTranslator, quirks, +) +from passlib.utils.compat import iteritems, get_method_function, u +from passlib.utils.decor import memoized_property +# tests +from passlib.tests.utils import TestCase, TEST_MODE, handler_derived_from +from passlib.tests.test_handlers import get_handler_case +# local +__all__ = [ + "DjangoBehaviorTest", + "ExtensionBehaviorTest", + "DjangoExtensionTest", + + "_ExtensionSupport", + "_ExtensionTest", +] +#============================================================================= +# configure django settings for testcases +#============================================================================= + +# whether we have supported django version +has_min_django = DJANGO_VERSION >= MIN_DJANGO_VERSION + +# import and configure empty django settings +# NOTE: we don't want to set up entirety of django, so not using django.setup() directly. +# instead, manually configuring the settings, and setting it up w/ no apps installed. +# in future, may need to alter this so we call django.setup() after setting +# DJANGO_SETTINGS_MODULE to a custom settings module w/ a dummy django app. +if has_min_django: + # + # initialize django settings manually + # + from django.conf import settings, LazySettings + + if not isinstance(settings, LazySettings): + # this probably means django globals have been configured already, + # which we don't want, since test cases reset and manipulate settings. + raise RuntimeError("expected django.conf.settings to be LazySettings: %r" % (settings,)) + + # else configure a blank settings instance for the unittests + if not settings.configured: + settings.configure() + + # + # init django apps w/ NO installed apps. + # NOTE: required for django >= 1.9 + # + from django.apps import apps + apps.populate(["django.contrib.contenttypes", "django.contrib.auth"]) + +# log a warning if tested w/ newer version. +# NOTE: this is mainly here as place to mark what version it was run against before release. +if DJANGO_VERSION >= (3, 2): + log.info("this release hasn't been tested against Django %r", DJANGO_VERSION) + +#============================================================================= +# support funcs +#============================================================================= + +# flag for update_settings() to remove specified key entirely +UNSET = object() + +def update_settings(**kwds): + """helper to update django settings from kwds""" + for k,v in iteritems(kwds): + if v is UNSET: + if hasattr(settings, k): + delattr(settings, k) + else: + setattr(settings, k, v) + +if has_min_django: + from django.contrib.auth.models import User + + class FakeUser(User): + """mock user object for use in testing""" + # NOTE: this mainly just overrides .save() to test commit behavior. + + # NOTE: .Meta.app_label required for django >= 1.9 + class Meta: + app_label = __name__ + + @memoized_property + def saved_passwords(self): + return [] + + def pop_saved_passwords(self): + try: + return self.saved_passwords[:] + finally: + del self.saved_passwords[:] + + def save(self, update_fields=None): + # NOTE: ignoring update_fields for test purposes + self.saved_passwords.append(self.password) + +def create_mock_setter(): + state = [] + def setter(password): + state.append(password) + def popstate(): + try: + return state[:] + finally: + del state[:] + setter.popstate = popstate + return setter + + +def check_django_hasher_has_backend(name): + """ + check whether django hasher is available; + or if it should be skipped because django lacks third-party library. + """ + assert name + from django.contrib.auth.hashers import make_password + try: + make_password("", hasher=name) + return True + except ValueError as err: + if re.match("Couldn't load '.*?' algorithm .* No module named .*", str(err)): + return False + raise + +#============================================================================= +# work up stock django config +#============================================================================= + +def _modify_django_config(kwds, sha_rounds=None): + """ + helper to build django CryptContext config matching expected setup for stock django deploy. + :param kwds: + :param sha_rounds: + :return: + """ + # make sure we have dict + if hasattr(kwds, "to_dict"): + # type: CryptContext + kwds = kwds.to_dict() + + # update defaults + kwds.update( + # TODO: push this to passlib.apps django contexts + deprecated="auto", + ) + + # fill in default rounds for current django version, so our sample hashes come back + # unchanged, instead of being upgraded in-place by check_password(). + if sha_rounds is None and has_min_django: + from django.contrib.auth.hashers import PBKDF2PasswordHasher + sha_rounds = PBKDF2PasswordHasher.iterations + + # modify rounds + if sha_rounds: + kwds.update( + django_pbkdf2_sha1__default_rounds=sha_rounds, + django_pbkdf2_sha256__default_rounds=sha_rounds, + ) + + return kwds + +#---------------------------------------------------- +# build config dict that matches stock django +#---------------------------------------------------- + +# XXX: replace this with code that interrogates default django config directly? +# could then separate out "validation of djangoXX_context objects" +# and "validation that individual hashers match django". +# or maybe add a "get_django_context(django_version)" helper to passlib.apps? +if DJANGO_VERSION >= (2, 1): + stock_config = _modify_django_config(_apps.django21_context) +elif DJANGO_VERSION >= (1, 10): + stock_config = _modify_django_config(_apps.django110_context) +else: + # assert DJANGO_VERSION >= (1, 8) + stock_config = _modify_django_config(_apps.django16_context) + +#---------------------------------------------------- +# override sample hashes used in test cases +#---------------------------------------------------- +from passlib.hash import django_pbkdf2_sha256 +sample_hashes = dict( + django_pbkdf2_sha256=("not a password", django_pbkdf2_sha256 + .using(rounds=stock_config.get("django_pbkdf2_sha256__default_rounds")) + .hash("not a password")) +) + +#============================================================================= +# test utils +#============================================================================= + +class _ExtensionSupport(object): + """ + test support funcs for loading/unloading extension. + this class is mixed in to various TestCase subclasses. + """ + #=================================================================== + # support funcs + #=================================================================== + + @classmethod + def _iter_patch_candidates(cls): + """helper to scan for monkeypatches. + + returns tuple containing: + * object (module or class) + * attribute of object + * value of attribute + * whether it should or should not be patched + """ + # XXX: this and assert_unpatched() could probably be refactored to use + # the PatchManager class to do the heavy lifting. + from django.contrib.auth import models, hashers + user_attrs = ["check_password", "set_password"] + model_attrs = ["check_password", "make_password"] + hasher_attrs = ["check_password", "make_password", "get_hasher", "identify_hasher", + "get_hashers"] + objs = [(models, model_attrs), + (models.User, user_attrs), + (hashers, hasher_attrs), + ] + for obj, patched in objs: + for attr in dir(obj): + if attr.startswith("_"): + continue + value = obj.__dict__.get(attr, UNSET) # can't use getattr() due to GAE + if value is UNSET and attr not in patched: + continue + value = get_method_function(value) + source = getattr(value, "__module__", None) + if source: + yield obj, attr, source, (attr in patched) + + #=================================================================== + # verify current patch state + #=================================================================== + + def assert_unpatched(self): + """ + test that django is in unpatched state + """ + # make sure we aren't currently patched + mod = sys.modules.get("passlib.ext.django.models") + self.assertFalse(mod and mod.adapter.patched, "patch should not be enabled") + + # make sure no objects have been replaced, by checking __module__ + for obj, attr, source, patched in self._iter_patch_candidates(): + if patched: + self.assertTrue(source.startswith("django.contrib.auth."), + "obj=%r attr=%r was not reverted: %r" % + (obj, attr, source)) + else: + self.assertFalse(source.startswith("passlib."), + "obj=%r attr=%r should not have been patched: %r" % + (obj, attr, source)) + + def assert_patched(self, context=None): + """ + helper to ensure django HAS been patched, and is using specified config + """ + # make sure we're currently patched + mod = sys.modules.get("passlib.ext.django.models") + self.assertTrue(mod and mod.adapter.patched, "patch should have been enabled") + + # make sure only the expected objects have been patched + for obj, attr, source, patched in self._iter_patch_candidates(): + if patched: + self.assertTrue(source == "passlib.ext.django.utils", + "obj=%r attr=%r should have been patched: %r" % + (obj, attr, source)) + else: + self.assertFalse(source.startswith("passlib."), + "obj=%r attr=%r should not have been patched: %r" % + (obj, attr, source)) + + # check context matches + if context is not None: + context = CryptContext._norm_source(context) + self.assertEqual(mod.password_context.to_dict(resolve=True), + context.to_dict(resolve=True)) + + #=================================================================== + # load / unload the extension (and verify it worked) + #=================================================================== + + _config_keys = ["PASSLIB_CONFIG", "PASSLIB_CONTEXT", "PASSLIB_GET_CATEGORY"] + + def load_extension(self, check=True, **kwds): + """ + helper to load extension with specified config & patch django + """ + self.unload_extension() + if check: + config = kwds.get("PASSLIB_CONFIG") or kwds.get("PASSLIB_CONTEXT") + for key in self._config_keys: + kwds.setdefault(key, UNSET) + update_settings(**kwds) + import passlib.ext.django.models + if check: + self.assert_patched(context=config) + + def unload_extension(self): + """ + helper to remove patches and unload extension + """ + # remove patches and unload module + mod = sys.modules.get("passlib.ext.django.models") + if mod: + mod.adapter.remove_patch() + del sys.modules["passlib.ext.django.models"] + # wipe config from django settings + update_settings(**dict((key, UNSET) for key in self._config_keys)) + # check everything's gone + self.assert_unpatched() + + #=================================================================== + # eoc + #=================================================================== + + +# XXX: rename to ExtensionFixture? +# NOTE: would roll this into _ExtensionSupport class; +# but we have to mix that one into django's TestCase classes as well; +# and our TestCase class (and this setUp() method) would foul things up. +class _ExtensionTest(TestCase, _ExtensionSupport): + """ + TestCase mixin which makes sure extension is unloaded before test; + and make sure it's unloaded after test as well. + """ + #============================================================================= + # setup + #============================================================================= + + def setUp(self): + super(_ExtensionTest, self).setUp() + + self.require_TEST_MODE("default") + + if not DJANGO_VERSION: + raise self.skipTest("Django not installed") + elif not has_min_django: + raise self.skipTest("Django version too old") + + # reset to baseline, and verify it worked + self.unload_extension() + + # and do the same when the test exits + self.addCleanup(self.unload_extension) + + #============================================================================= + # eoc + #============================================================================= + +#============================================================================= +# extension tests +#============================================================================= + +#: static passwords used by DjangoBehaviorTest methods +PASS1 = "toomanysecrets" +WRONG1 = "letmein" + + +class DjangoBehaviorTest(_ExtensionTest): + """ + tests model to verify it matches django's behavior. + + running this class verifies the tests correctly assert what Django itself does. + + running the ExtensionBehaviorTest subclass below verifies "passlib.ext.django" + matches what the tests assert. + """ + #============================================================================= + # class attrs + #============================================================================= + + descriptionPrefix = "verify django behavior" + + #: tracks whether tests should assume "passlib.ext.django" monkeypatch is applied. + #: (set to True by ExtensionBehaviorTest subclass) + patched = False + + #: dict containing CryptContext() config which should match current django deploy. + #: used by tests to verify expected behavior. + config = stock_config + + # NOTE: if this test fails, it means we're not accounting for + # some part of django's hashing logic, or that this is + # running against an untested version of django with a new + # hashing policy. + + #============================================================================= + # test helpers + #============================================================================= + + @memoized_property + def context(self): + """ + per-test CryptContext() created from .config. + """ + return CryptContext._norm_source(self.config) + + def assert_unusable_password(self, user): + """ + check that user object is set to 'unusable password' constant + """ + self.assertTrue(user.password.startswith("!")) + self.assertFalse(user.has_usable_password()) + self.assertEqual(user.pop_saved_passwords(), []) + + def assert_valid_password(self, user, hash=UNSET, saved=None): + """ + check that user object has a usable password hash. + :param hash: optionally check it has this exact hash + :param saved: check that mock commit history for user.password matches this list + """ + if hash is UNSET: + self.assertNotEqual(user.password, "!") + self.assertNotEqual(user.password, None) + else: + self.assertEqual(user.password, hash) + self.assertTrue(user.has_usable_password(), + "hash should be usable: %r" % (user.password,)) + self.assertEqual(user.pop_saved_passwords(), + [] if saved is None else [saved]) + + #============================================================================= + # test hashing interface + #----------------------------------------------------------------------------- + # these functions are run against both the actual django code, + # to verify the assumptions of the unittests are correct; + # and run against the passlib extension, to verify it matches those assumptions. + # + # these tests check the following django methods: + # User.set_password() + # User.check_password() + # make_password() -- 1.4 only + # check_password() + # identify_hasher() + # User.has_usable_password() + # User.set_unusable_password() + # + # XXX: this take a while to run. what could be trimmed? + # + # TODO: add get_hasher() checks where appropriate in tests below. + #============================================================================= + + def test_extension_config(self): + """ + test extension config is loaded correctly + """ + if not self.patched: + raise self.skipTest("extension not loaded") + + ctx = self.context + + # contexts should match + from django.contrib.auth.hashers import check_password + from passlib.ext.django.models import password_context + self.assertEqual(password_context.to_dict(resolve=True), ctx.to_dict(resolve=True)) + + # should have patched both places + from django.contrib.auth.models import check_password as check_password2 + self.assertEqual(check_password2, check_password) + + def test_default_algorithm(self): + """ + test django's default algorithm + """ + ctx = self.context + + # NOTE: import has to be done w/in method, in case monkeypatching is applied by setUp() + from django.contrib.auth.hashers import make_password + + # User.set_password() should use default alg + user = FakeUser() + user.set_password(PASS1) + self.assertTrue(ctx.handler().verify(PASS1, user.password)) + self.assert_valid_password(user) + + # User.check_password() - n/a + + # make_password() should use default alg + hash = make_password(PASS1) + self.assertTrue(ctx.handler().verify(PASS1, hash)) + + # check_password() - n/a + + def test_empty_password(self): + """ + test how methods handle empty string as password + """ + ctx = self.context + + # NOTE: import has to be done w/in method, in case monkeypatching is applied by setUp() + from django.contrib.auth.hashers import ( + check_password, + make_password, + is_password_usable, + identify_hasher, + ) + + # User.set_password() should use default alg + user = FakeUser() + user.set_password('') + hash = user.password + self.assertTrue(ctx.handler().verify('', hash)) + self.assert_valid_password(user, hash) + + # User.check_password() should return True + self.assertTrue(user.check_password("")) + self.assert_valid_password(user, hash) + + # XXX: test make_password() ? + + # TODO: is_password_usable() + + # identify_hasher() -- na + + # check_password() should return True + self.assertTrue(check_password("", hash)) + + def test_unusable_flag(self): + """ + test how methods handle 'unusable flag' in hash + """ + # NOTE: import has to be done w/in method, in case monkeypatching is applied by setUp() + from django.contrib.auth.hashers import ( + check_password, + make_password, + is_password_usable, + identify_hasher, + ) + + # sanity check via user.set_unusable_password() + user = FakeUser() + user.set_unusable_password() + self.assert_unusable_password(user) + + # ensure User.set_password() sets unusable flag + user = FakeUser() + user.set_password(None) + self.assert_unusable_password(user) + + # User.check_password() should always fail + self.assertFalse(user.check_password(None)) + self.assertFalse(user.check_password('None')) + self.assertFalse(user.check_password('')) + self.assertFalse(user.check_password(PASS1)) + self.assertFalse(user.check_password(WRONG1)) + self.assert_unusable_password(user) + + # make_password() should also set flag + self.assertTrue(make_password(None).startswith("!")) + + # check_password() should return False (didn't handle disabled under 1.3) + self.assertFalse(check_password(PASS1, '!')) + + # identify_hasher() and is_password_usable() should reject it + self.assertFalse(is_password_usable(user.password)) + self.assertRaises(ValueError, identify_hasher, user.password) + + def test_none_hash_value(self): + """ + test how methods handle None as hash value + """ + patched = self.patched + + # NOTE: import has to be done w/in method, in case monkeypatching is applied by setUp() + from django.contrib.auth.hashers import ( + check_password, + make_password, + is_password_usable, + identify_hasher, + ) + + # User.set_password() - n/a + + # User.check_password() - returns False + user = FakeUser() + user.password = None + if quirks.none_causes_check_password_error and not patched: + # django 2.1+ + self.assertRaises(TypeError, user.check_password, PASS1) + else: + self.assertFalse(user.check_password(PASS1)) + + self.assertEqual(user.has_usable_password(), + quirks.empty_is_usable_password) + + # TODO: is_password_usable() + + # make_password() - n/a + + # check_password() - error + if quirks.none_causes_check_password_error and not patched: + self.assertRaises(TypeError, check_password, PASS1, None) + else: + self.assertFalse(check_password(PASS1, None)) + + # identify_hasher() - error + self.assertRaises(TypeError, identify_hasher, None) + + def test_empty_hash_value(self): + """ + test how methods handle empty string as hash value + """ + # NOTE: import has to be done w/in method, in case monkeypatching is applied by setUp() + from django.contrib.auth.hashers import ( + check_password, + make_password, + is_password_usable, + identify_hasher, + ) + + # User.set_password() - n/a + + # User.check_password() + # As of django 1.5, blank hash returns False (django issue 18453) + user = FakeUser() + user.password = "" + self.assertFalse(user.check_password(PASS1)) + + # verify hash wasn't changed/upgraded during check_password() call + self.assertEqual(user.password, "") + self.assertEqual(user.pop_saved_passwords(), []) + + # User.has_usable_password() + self.assertEqual(user.has_usable_password(), quirks.empty_is_usable_password) + + # TODO: is_password_usable() + + # make_password() - n/a + + # check_password() + self.assertFalse(check_password(PASS1, "")) + + # identify_hasher() - throws error + self.assertRaises(ValueError, identify_hasher, "") + + def test_invalid_hash_values(self): + """ + test how methods handle invalid hash values. + """ + for hash in [ + "$789$foo", # empty identifier + ]: + with self.subTest(hash=hash): + self._do_test_invalid_hash_value(hash) + + def _do_test_invalid_hash_value(self, hash): + + # NOTE: import has to be done w/in method, in case monkeypatching is applied by setUp() + from django.contrib.auth.hashers import ( + check_password, + make_password, + is_password_usable, + identify_hasher, + ) + + # User.set_password() - n/a + + # User.check_password() + # As of django 1.5, invalid hash returns False (side effect of django issue 18453) + user = FakeUser() + user.password = hash + self.assertFalse(user.check_password(PASS1)) + + # verify hash wasn't changed/upgraded during check_password() call + self.assertEqual(user.password, hash) + self.assertEqual(user.pop_saved_passwords(), []) + + # User.has_usable_password() + self.assertEqual(user.has_usable_password(), quirks.invalid_is_usable_password) + + # TODO: is_password_usable() + + # make_password() - n/a + + # check_password() + self.assertFalse(check_password(PASS1, hash)) + + # identify_hasher() - throws error + self.assertRaises(ValueError, identify_hasher, hash) + + def test_available_schemes(self): + """ + run a bunch of subtests for each hasher available in the default django setup + (as determined by reading self.context) + """ + for scheme in self.context.schemes(): + with self.subTest(scheme=scheme): + self._do_test_available_scheme(scheme) + + def _do_test_available_scheme(self, scheme): + """ + helper to test how specific hasher behaves. + :param scheme: *passlib* name of hasher (e.g. "django_pbkdf2_sha256") + """ + log = self.getLogger() + ctx = self.context + patched = self.patched + setter = create_mock_setter() + + # NOTE: import has to be done w/in method, in case monkeypatching is applied by setUp() + from django.contrib.auth.hashers import ( + check_password, + make_password, + is_password_usable, + identify_hasher, + ) + + #------------------------------------------------------- + # setup constants & imports, pick a sample secret/hash combo + #------------------------------------------------------- + handler = ctx.handler(scheme) + log.debug("testing scheme: %r => %r", scheme, handler) + deprecated = ctx.handler(scheme).deprecated + assert not deprecated or scheme != ctx.default_scheme() + try: + testcase = get_handler_case(scheme) + except exc.MissingBackendError: + raise self.skipTest("backend not available") + assert handler_derived_from(handler, testcase.handler) + if handler.is_disabled: + raise self.skipTest("skip disabled hasher") + + # verify that django has a backend available + # (since our hasher may use different set of backends, + # get_handler_case() above may work, but django will have nothing) + if not patched and not check_django_hasher_has_backend(handler.django_name): + assert scheme in ["django_bcrypt", "django_bcrypt_sha256", "django_argon2"], \ + "%r scheme should always have active backend" % scheme + log.warning("skipping scheme %r due to missing django dependency", scheme) + raise self.skipTest("skip due to missing dependency") + + # find a sample (secret, hash) pair to test with + try: + secret, hash = sample_hashes[scheme] + except KeyError: + get_sample_hash = testcase("setUp").get_sample_hash + while True: + secret, hash = get_sample_hash() + if secret: # don't select blank passwords + break + other = 'dontletmein' + + #------------------------------------------------------- + # User.set_password() - not tested here + #------------------------------------------------------- + + #------------------------------------------------------- + # User.check_password()+migration against known hash + #------------------------------------------------------- + user = FakeUser() + user.password = hash + + # check against invalid password + self.assertFalse(user.check_password(None)) + ##self.assertFalse(user.check_password('')) + self.assertFalse(user.check_password(other)) + self.assert_valid_password(user, hash) + + # check against valid password + self.assertTrue(user.check_password(secret)) + + # check if it upgraded the hash + # NOTE: needs_update kept separate in case we need to test rounds. + needs_update = deprecated + if needs_update: + self.assertNotEqual(user.password, hash) + self.assertFalse(handler.identify(user.password)) + self.assertTrue(ctx.handler().verify(secret, user.password)) + self.assert_valid_password(user, saved=user.password) + else: + self.assert_valid_password(user, hash) + + # don't need to check rest for most deployments + if TEST_MODE(max="default"): + return + + #------------------------------------------------------- + # make_password() correctly selects algorithm + #------------------------------------------------------- + alg = DjangoTranslator().passlib_to_django_name(scheme) + hash2 = make_password(secret, hasher=alg) + self.assertTrue(handler.verify(secret, hash2)) + + #------------------------------------------------------- + # check_password()+setter against known hash + #------------------------------------------------------- + # should call setter only if it needs_update + self.assertTrue(check_password(secret, hash, setter=setter)) + self.assertEqual(setter.popstate(), [secret] if needs_update else []) + + # should not call setter + self.assertFalse(check_password(other, hash, setter=setter)) + self.assertEqual(setter.popstate(), []) + + ### check preferred kwd is ignored (feature we don't currently support fully) + ##self.assertTrue(check_password(secret, hash, setter=setter, preferred='fooey')) + ##self.assertEqual(setter.popstate(), [secret]) + + # TODO: get_hasher() + + #------------------------------------------------------- + # identify_hasher() recognizes known hash + #------------------------------------------------------- + self.assertTrue(is_password_usable(hash)) + name = DjangoTranslator().django_to_passlib_name(identify_hasher(hash).algorithm) + self.assertEqual(name, scheme) + + #=================================================================== + # eoc + #=================================================================== + +#=================================================================== +# extension fidelity tests +#=================================================================== + +class ExtensionBehaviorTest(DjangoBehaviorTest): + """ + test that "passlib.ext.django" conforms to behavioral assertions in DjangoBehaviorTest + """ + descriptionPrefix = "verify extension behavior" + + config = dict( + schemes="sha256_crypt,md5_crypt,des_crypt", + deprecated="des_crypt", + ) + + def setUp(self): + super(ExtensionBehaviorTest, self).setUp() + + # always load extension before each test + self.load_extension(PASSLIB_CONFIG=self.config) + self.patched = True + +#=================================================================== +# extension internal tests +#=================================================================== + +class DjangoExtensionTest(_ExtensionTest): + """ + test the ``passlib.ext.django`` plugin + """ + #=================================================================== + # class attrs + #=================================================================== + + descriptionPrefix = "passlib.ext.django plugin" + + #=================================================================== + # monkeypatch testing + #=================================================================== + + def test_00_patch_control(self): + """test set_django_password_context patch/unpatch""" + + # check config="disabled" + self.load_extension(PASSLIB_CONFIG="disabled", check=False) + self.assert_unpatched() + + # check legacy config=None + with self.assertWarningList("PASSLIB_CONFIG=None is deprecated"): + self.load_extension(PASSLIB_CONFIG=None, check=False) + self.assert_unpatched() + + # try stock django 1.0 context + self.load_extension(PASSLIB_CONFIG="django-1.0", check=False) + self.assert_patched(context=django10_context) + + # try to remove patch + self.unload_extension() + + # patch to use stock django 1.4 context + self.load_extension(PASSLIB_CONFIG="django-1.4", check=False) + self.assert_patched(context=django14_context) + + # try to remove patch again + self.unload_extension() + + def test_01_overwrite_detection(self): + """test detection of foreign monkeypatching""" + # NOTE: this sets things up, and spot checks two methods, + # this should be enough to verify patch manager is working. + # TODO: test unpatch behavior honors flag. + + # configure plugin to use sample context + config = "[passlib]\nschemes=des_crypt\n" + self.load_extension(PASSLIB_CONFIG=config) + + # setup helpers + import django.contrib.auth.models as models + from passlib.ext.django.models import adapter + def dummy(): + pass + + # mess with User.set_password, make sure it's detected + orig = models.User.set_password + models.User.set_password = dummy + with self.assertWarningList("another library has patched.*User\.set_password"): + adapter._manager.check_all() + models.User.set_password = orig + + # mess with models.check_password, make sure it's detected + orig = models.check_password + models.check_password = dummy + with self.assertWarningList("another library has patched.*models:check_password"): + adapter._manager.check_all() + models.check_password = orig + + def test_02_handler_wrapper(self): + """test Hasher-compatible handler wrappers""" + from django.contrib.auth import hashers + + passlib_to_django = DjangoTranslator().passlib_to_django + + # should return native django hasher if available + if DJANGO_VERSION > (1, 10): + self.assertRaises(ValueError, passlib_to_django, "hex_md5") + else: + hasher = passlib_to_django("hex_md5") + self.assertIsInstance(hasher, hashers.UnsaltedMD5PasswordHasher) + + # should return native django hasher + # NOTE: present but not enabled by default in django as of 2.1 + # (see _builtin_django_hashers) + hasher = passlib_to_django("django_bcrypt") + self.assertIsInstance(hasher, hashers.BCryptPasswordHasher) + + # otherwise should return wrapper + from passlib.hash import sha256_crypt + hasher = passlib_to_django("sha256_crypt") + self.assertEqual(hasher.algorithm, "passlib_sha256_crypt") + + # and wrapper should return correct hash + encoded = hasher.encode("stub") + self.assertTrue(sha256_crypt.verify("stub", encoded)) + self.assertTrue(hasher.verify("stub", encoded)) + self.assertFalse(hasher.verify("xxxx", encoded)) + + # test wrapper accepts options + encoded = hasher.encode("stub", "abcd"*4, rounds=1234) + self.assertEqual(encoded, "$5$rounds=1234$abcdabcdabcdabcd$" + "v2RWkZQzctPdejyRqmmTDQpZN6wTh7.RUy9zF2LftT6") + self.assertEqual(hasher.safe_summary(encoded), + {'algorithm': 'sha256_crypt', + 'salt': u('abcdab**********'), + 'rounds': 1234, + 'hash': u('v2RWkZ*************************************'), + }) + + # made up name should throw error + # XXX: should this throw ValueError instead, to match django? + self.assertRaises(KeyError, passlib_to_django, "does_not_exist") + + #=================================================================== + # PASSLIB_CONFIG settings + #=================================================================== + def test_11_config_disabled(self): + """test PASSLIB_CONFIG='disabled'""" + # test config=None (deprecated) + with self.assertWarningList("PASSLIB_CONFIG=None is deprecated"): + self.load_extension(PASSLIB_CONFIG=None, check=False) + self.assert_unpatched() + + # test disabled config + self.load_extension(PASSLIB_CONFIG="disabled", check=False) + self.assert_unpatched() + + def test_12_config_presets(self): + """test PASSLIB_CONFIG=''""" + # test django presets + self.load_extension(PASSLIB_CONTEXT="django-default", check=False) + ctx = django16_context + self.assert_patched(ctx) + + self.load_extension(PASSLIB_CONFIG="django-1.0", check=False) + self.assert_patched(django10_context) + + self.load_extension(PASSLIB_CONFIG="django-1.4", check=False) + self.assert_patched(django14_context) + + def test_13_config_defaults(self): + """test PASSLIB_CONFIG default behavior""" + # check implicit default + from passlib.ext.django.utils import PASSLIB_DEFAULT + default = CryptContext.from_string(PASSLIB_DEFAULT) + self.load_extension() + self.assert_patched(PASSLIB_DEFAULT) + + # check default preset + self.load_extension(PASSLIB_CONTEXT="passlib-default", check=False) + self.assert_patched(PASSLIB_DEFAULT) + + # check explicit string + self.load_extension(PASSLIB_CONTEXT=PASSLIB_DEFAULT, check=False) + self.assert_patched(PASSLIB_DEFAULT) + + def test_14_config_invalid(self): + """test PASSLIB_CONFIG type checks""" + update_settings(PASSLIB_CONTEXT=123, PASSLIB_CONFIG=UNSET) + self.assertRaises(TypeError, __import__, 'passlib.ext.django.models') + + self.unload_extension() + update_settings(PASSLIB_CONFIG="missing-preset", PASSLIB_CONTEXT=UNSET) + self.assertRaises(ValueError, __import__, 'passlib.ext.django.models') + + #=================================================================== + # PASSLIB_GET_CATEGORY setting + #=================================================================== + def test_21_category_setting(self): + """test PASSLIB_GET_CATEGORY parameter""" + # define config where rounds can be used to detect category + config = dict( + schemes = ["sha256_crypt"], + sha256_crypt__default_rounds = 1000, + staff__sha256_crypt__default_rounds = 2000, + superuser__sha256_crypt__default_rounds = 3000, + ) + from passlib.hash import sha256_crypt + + def run(**kwds): + """helper to take in user opts, return rounds used in password""" + user = FakeUser(**kwds) + user.set_password("stub") + return sha256_crypt.from_string(user.password).rounds + + # test default get_category + self.load_extension(PASSLIB_CONFIG=config) + self.assertEqual(run(), 1000) + self.assertEqual(run(is_staff=True), 2000) + self.assertEqual(run(is_superuser=True), 3000) + + # test patch uses explicit get_category function + def get_category(user): + return user.first_name or None + self.load_extension(PASSLIB_CONTEXT=config, + PASSLIB_GET_CATEGORY=get_category) + self.assertEqual(run(), 1000) + self.assertEqual(run(first_name='other'), 1000) + self.assertEqual(run(first_name='staff'), 2000) + self.assertEqual(run(first_name='superuser'), 3000) + + # test patch can disable get_category entirely + def get_category(user): + return None + self.load_extension(PASSLIB_CONTEXT=config, + PASSLIB_GET_CATEGORY=get_category) + self.assertEqual(run(), 1000) + self.assertEqual(run(first_name='other'), 1000) + self.assertEqual(run(first_name='staff', is_staff=True), 1000) + self.assertEqual(run(first_name='superuser', is_superuser=True), 1000) + + # test bad value + self.assertRaises(TypeError, self.load_extension, PASSLIB_CONTEXT=config, + PASSLIB_GET_CATEGORY='x') + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/test_ext_django_source.py b/ansible/lib/python3.11/site-packages/passlib/tests/test_ext_django_source.py new file mode 100644 index 000000000..4b42e59bc --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/tests/test_ext_django_source.py @@ -0,0 +1,250 @@ +""" +test passlib.ext.django against django source tests +""" +#============================================================================= +# imports +#============================================================================= +from __future__ import absolute_import, division, print_function +# core +import logging; log = logging.getLogger(__name__) +# site +# pkg +from passlib.utils.compat import suppress_cause +from passlib.ext.django.utils import DJANGO_VERSION, DjangoTranslator, _PasslibHasherWrapper +# tests +from passlib.tests.utils import TestCase, TEST_MODE +from .test_ext_django import ( + has_min_django, stock_config, _ExtensionSupport, +) +if has_min_django: + from .test_ext_django import settings +# local +__all__ = [ + "HashersTest", +] +#============================================================================= +# HashersTest -- +# hack up the some of the real django tests to run w/ extension loaded, +# to ensure we mimic their behavior. +# however, the django tests were moved out of the package, and into a source-only location +# as of django 1.7. so we disable tests from that point on unless test-runner specifies +#============================================================================= + +#: ref to django unittest root module (if found) +test_hashers_mod = None + +#: message about why test module isn't present (if not found) +hashers_skip_msg = None + +#---------------------------------------------------------------------- +# try to load django's tests/auth_tests/test_hasher.py module, +# or note why we failed. +#---------------------------------------------------------------------- +if TEST_MODE(max="quick"): + hashers_skip_msg = "requires >= 'default' test mode" + +elif has_min_django: + import os + import sys + source_path = os.environ.get("PASSLIB_TESTS_DJANGO_SOURCE_PATH") + + if source_path: + if not os.path.exists(source_path): + raise EnvironmentError("django source path not found: %r" % source_path) + if not all(os.path.exists(os.path.join(source_path, name)) + for name in ["django", "tests"]): + raise EnvironmentError("invalid django source path: %r" % source_path) + log.info("using django tests from source path: %r", source_path) + tests_path = os.path.join(source_path, "tests") + sys.path.insert(0, tests_path) + try: + from auth_tests import test_hashers as test_hashers_mod + except ImportError as err: + raise suppress_cause( + EnvironmentError("error trying to import django tests " + "from source path (%r): %r" % + (source_path, err))) + finally: + sys.path.remove(tests_path) + + else: + hashers_skip_msg = "requires PASSLIB_TESTS_DJANGO_SOURCE_PATH to be set" + + if TEST_MODE("full"): + # print warning so user knows what's happening + sys.stderr.write("\nWARNING: $PASSLIB_TESTS_DJANGO_SOURCE_PATH is not set; " + "can't run Django's own unittests against passlib.ext.django\n") + +elif DJANGO_VERSION: + hashers_skip_msg = "django version too old" + +else: + hashers_skip_msg = "django not installed" + +#---------------------------------------------------------------------- +# if found module, create wrapper to run django's own tests, +# but with passlib monkeypatched in. +#---------------------------------------------------------------------- +if test_hashers_mod: + from django.core.signals import setting_changed + from django.dispatch import receiver + from django.utils.module_loading import import_string + from passlib.utils.compat import get_unbound_method_function + + class HashersTest(test_hashers_mod.TestUtilsHashPass, _ExtensionSupport): + """ + Run django's hasher unittests against passlib's extension + and workalike implementations + """ + + #================================================================== + # helpers + #================================================================== + + # port patchAttr() helper method from passlib.tests.utils.TestCase + patchAttr = get_unbound_method_function(TestCase.patchAttr) + + #================================================================== + # custom setup + #================================================================== + def setUp(self): + #--------------------------------------------------------- + # install passlib.ext.django adapter, and get context + #--------------------------------------------------------- + self.load_extension(PASSLIB_CONTEXT=stock_config, check=False) + from passlib.ext.django.models import adapter + context = adapter.context + + #--------------------------------------------------------- + # patch tests module to use our versions of patched funcs + # (which should be installed in hashers module) + #--------------------------------------------------------- + from django.contrib.auth import hashers + for attr in ["make_password", + "check_password", + "identify_hasher", + "is_password_usable", + "get_hasher"]: + self.patchAttr(test_hashers_mod, attr, getattr(hashers, attr)) + + #--------------------------------------------------------- + # django tests expect empty django_des_crypt salt field + #--------------------------------------------------------- + from passlib.hash import django_des_crypt + self.patchAttr(django_des_crypt, "use_duplicate_salt", False) + + #--------------------------------------------------------- + # install receiver to update scheme list if test changes settings + #--------------------------------------------------------- + django_to_passlib_name = DjangoTranslator().django_to_passlib_name + + @receiver(setting_changed, weak=False) + def update_schemes(**kwds): + if kwds and kwds['setting'] != 'PASSWORD_HASHERS': + return + assert context is adapter.context + schemes = [ + django_to_passlib_name(import_string(hash_path)()) + for hash_path in settings.PASSWORD_HASHERS + ] + # workaround for a few tests that only specify hex_md5, + # but test for django_salted_md5 format. + if "hex_md5" in schemes and "django_salted_md5" not in schemes: + schemes.append("django_salted_md5") + schemes.append("django_disabled") + context.update(schemes=schemes, deprecated="auto") + adapter.reset_hashers() + + self.addCleanup(setting_changed.disconnect, update_schemes) + + update_schemes() + + #--------------------------------------------------------- + # need password_context to keep up to date with django_hasher.iterations, + # which is frequently patched by django tests. + # + # HACK: to fix this, inserting wrapper around a bunch of context + # methods so that any time adapter calls them, + # attrs are resynced first. + #--------------------------------------------------------- + + def update_rounds(): + """ + sync django hasher config -> passlib hashers + """ + for handler in context.schemes(resolve=True): + if 'rounds' not in handler.setting_kwds: + continue + hasher = adapter.passlib_to_django(handler) + if isinstance(hasher, _PasslibHasherWrapper): + continue + rounds = getattr(hasher, "rounds", None) or \ + getattr(hasher, "iterations", None) + if rounds is None: + continue + # XXX: this doesn't modify the context, which would + # cause other weirdness (since it would replace handler factories completely, + # instead of just updating their state) + handler.min_desired_rounds = handler.max_desired_rounds = handler.default_rounds = rounds + + _in_update = [False] + + def update_wrapper(wrapped, *args, **kwds): + """ + wrapper around arbitrary func, that first triggers sync + """ + if not _in_update[0]: + _in_update[0] = True + try: + update_rounds() + finally: + _in_update[0] = False + return wrapped(*args, **kwds) + + # sync before any context call + for attr in ["schemes", "handler", "default_scheme", "hash", + "verify", "needs_update", "verify_and_update"]: + self.patchAttr(context, attr, update_wrapper, wrap=True) + + # sync whenever adapter tries to resolve passlib hasher + self.patchAttr(adapter, "django_to_passlib", update_wrapper, wrap=True) + + def tearDown(self): + # NOTE: could rely on addCleanup() instead, but need py26 compat + self.unload_extension() + super(HashersTest, self).tearDown() + + #================================================================== + # skip a few methods that can't be replicated properly + # *want to minimize these as much as possible* + #================================================================== + + _OMIT = lambda self: self.skipTest("omitted by passlib") + + # XXX: this test registers two classes w/ same algorithm id, + # something we don't support -- how does django sanely handle + # that anyways? get_hashers_by_algorithm() should throw KeyError, right? + test_pbkdf2_upgrade_new_hasher = _OMIT + + # TODO: support wrapping django's harden-runtime feature? + # would help pass their tests. + test_check_password_calls_harden_runtime = _OMIT + test_bcrypt_harden_runtime = _OMIT + test_pbkdf2_harden_runtime = _OMIT + + #================================================================== + # eoc + #================================================================== + +else: + # otherwise leave a stub so test log tells why test was skipped. + + class HashersTest(TestCase): + + def test_external_django_hasher_tests(self): + """external django hasher tests""" + raise self.skipTest(hashers_skip_msg) + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/test_handlers.py b/ansible/lib/python3.11/site-packages/passlib/tests/test_handlers.py new file mode 100644 index 000000000..cad5ef992 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/tests/test_handlers.py @@ -0,0 +1,1819 @@ +"""passlib.tests.test_handlers - tests for passlib hash algorithms""" +#============================================================================= +# imports +#============================================================================= +from __future__ import with_statement +# core +import logging; log = logging.getLogger(__name__) +import os +import sys +import warnings +# site +# pkg +from passlib import exc, hash +from passlib.utils import repeat_string +from passlib.utils.compat import irange, PY3, u, get_method_function +from passlib.tests.utils import TestCase, HandlerCase, skipUnless, \ + TEST_MODE, UserHandlerMixin, EncodingHandlerMixin +# module + +#============================================================================= +# constants & support +#============================================================================= + +# some common unicode passwords which used as test cases +UPASS_WAV = u('\u0399\u03c9\u03b1\u03bd\u03bd\u03b7\u03c2') +UPASS_USD = u("\u20AC\u00A5$") +UPASS_TABLE = u("t\u00e1\u0411\u2113\u0259") + +PASS_TABLE_UTF8 = b't\xc3\xa1\xd0\x91\xe2\x84\x93\xc9\x99' # utf-8 + +# handlers which support multiple backends, but don't have multi-backend tests. +_omitted_backend_tests = ["django_bcrypt", "django_bcrypt_sha256", "django_argon2"] + +#: modules where get_handler_case() should search for test cases. +_handler_test_modules = [ + "test_handlers", + "test_handlers_argon2", + "test_handlers_bcrypt", + "test_handlers_cisco", + "test_handlers_django", + "test_handlers_pbkdf2", + "test_handlers_scrypt", +] + +def get_handler_case(scheme): + """ + return HandlerCase instance for scheme, used by other tests. + + :param scheme: name of hasher to locate test for (e.g. "bcrypt") + + :raises KeyError: + if scheme isn't known hasher. + + :raises MissingBackendError: + if hasher doesn't have any available backends. + + :returns: + HandlerCase subclass (which derives from TestCase) + """ + from passlib.registry import get_crypt_handler + handler = get_crypt_handler(scheme) + if hasattr(handler, "backends") and scheme not in _omitted_backend_tests: + # XXX: if no backends available, could proceed to pick first backend for test lookup; + # should investigate if that would be useful to callers. + try: + backend = handler.get_backend() + except exc.MissingBackendError: + assert scheme in conditionally_available_hashes + raise + name = "%s_%s_test" % (scheme, backend) + else: + name = "%s_test" % scheme + for module in _handler_test_modules: + modname = "passlib.tests." + module + __import__(modname) + mod = sys.modules[modname] + try: + return getattr(mod, name) + except AttributeError: + pass + # every hasher should have test suite, so if we get here, means test is either missing, + # misnamed, or _handler_test_modules list is out of date. + raise RuntimeError("can't find test case named %r for %r" % (name, scheme)) + +#: hashes which there may not be a backend available for, +#: and get_handler_case() may (correctly) throw a MissingBackendError +conditionally_available_hashes = ["argon2", "bcrypt", "bcrypt_sha256"] + +#============================================================================= +# apr md5 crypt +#============================================================================= +class apr_md5_crypt_test(HandlerCase): + handler = hash.apr_md5_crypt + + known_correct_hashes = [ + # + # http://httpd.apache.org/docs/2.2/misc/password_encryptions.html + # + ('myPassword', '$apr1$r31.....$HqJZimcKQFAMYayBlzkrA/'), + + # + # custom + # + + # ensures utf-8 used for unicode + (UPASS_TABLE, '$apr1$bzYrOHUx$a1FcpXuQDJV3vPY20CS6N1'), + ] + + known_malformed_hashes = [ + # bad char in otherwise correct hash ----\/ + '$apr1$r31.....$HqJZimcKQFAMYayBlzkrA!' + ] + +#============================================================================= +# bigcrypt +#============================================================================= +class bigcrypt_test(HandlerCase): + handler = hash.bigcrypt + + # TODO: find an authoritative source of test vectors + known_correct_hashes = [ + + # + # various docs & messages on the web. + # + ("passphrase", "qiyh4XPJGsOZ2MEAyLkfWqeQ"), + ("This is very long passwd", "f8.SVpL2fvwjkAnxn8/rgTkwvrif6bjYB5c"), + + # + # custom + # + + # ensures utf-8 used for unicode + (UPASS_TABLE, 'SEChBAyMbMNhgGLyP7kD1HZU'), + ] + + known_unidentified_hashes = [ + # one char short (10 % 11) + "qiyh4XPJGsOZ2MEAyLkfWqe" + + # one char too many (1 % 11) + "f8.SVpL2fvwjkAnxn8/rgTkwvrif6bjYB5cd" + ] + + # omit des_crypt from known_other since it's a valid bigcrypt hash too. + known_other_hashes = [row for row in HandlerCase.known_other_hashes + if row[0] != "des_crypt"] + + def test_90_internal(self): + # check that _norm_checksum() also validates checksum size. + # (current code uses regex in parser) + self.assertRaises(ValueError, hash.bigcrypt, use_defaults=True, + checksum=u('yh4XPJGsOZ')) + +#============================================================================= +# bsdi crypt +#============================================================================= +class _bsdi_crypt_test(HandlerCase): + """test BSDiCrypt algorithm""" + handler = hash.bsdi_crypt + + known_correct_hashes = [ + # + # from JTR 1.7.9 + # + ('U*U*U*U*', '_J9..CCCCXBrJUJV154M'), + ('U*U***U', '_J9..CCCCXUhOBTXzaiE'), + ('U*U***U*', '_J9..CCCC4gQ.mB/PffM'), + ('*U*U*U*U', '_J9..XXXXvlzQGqpPPdk'), + ('*U*U*U*U*', '_J9..XXXXsqM/YSSP..Y'), + ('*U*U*U*U*U*U*U*U', '_J9..XXXXVL7qJCnku0I'), + ('*U*U*U*U*U*U*U*U*', '_J9..XXXXAj8cFbP5scI'), + ('ab1234567', '_J9..SDizh.vll5VED9g'), + ('cr1234567', '_J9..SDizRjWQ/zePPHc'), + ('zxyDPWgydbQjgq', '_J9..SDizxmRI1GjnQuE'), + ('726 even', '_K9..SaltNrQgIYUAeoY'), + ('', '_J9..SDSD5YGyRCr4W4c'), + + # + # custom + # + (" ", "_K1..crsmZxOLzfJH8iw"), + ("my", '_KR/.crsmykRplHbAvwA'), # <-- to detect old 12-bit rounds bug + ("my socra", "_K1..crsmf/9NzZr1fLM"), + ("my socrates", '_K1..crsmOv1rbde9A9o'), + ("my socrates note", "_K1..crsm/2qeAhdISMA"), + + # ensures utf-8 used for unicode + (UPASS_TABLE, '_7C/.ABw0WIKy0ILVqo2'), + ] + known_unidentified_hashes = [ + # bad char in otherwise correctly formatted hash + # \/ + "_K1.!crsmZxOLzfJH8iw" + ] + + platform_crypt_support = [ + # openbsd 5.8 dropped everything except bcrypt + ("openbsd[6789]", False), + ("openbsd5", None), + ("openbsd", True), + + ("freebsd|netbsd|darwin", True), + ("solaris", False), + ("linux", None), # may be present if libxcrypt is in use + ] + + def test_77_fuzz_input(self, **kwds): + # we want to generate even rounds to verify it's correct, but want to ignore warnings + warnings.filterwarnings("ignore", "bsdi_crypt rounds should be odd.*") + super(_bsdi_crypt_test, self).test_77_fuzz_input(**kwds) + + def test_needs_update_w_even_rounds(self): + """needs_update() should flag even rounds""" + handler = self.handler + even_hash = '_Y/../cG0zkJa6LY6k4c' + odd_hash = '_Z/..TgFg0/ptQtpAgws' + secret = 'test' + + # don't issue warning + self.assertTrue(handler.verify(secret, even_hash)) + self.assertTrue(handler.verify(secret, odd_hash)) + + # *do* signal as needing updates + self.assertTrue(handler.needs_update(even_hash)) + self.assertFalse(handler.needs_update(odd_hash)) + + # new hashes shouldn't have even rounds + new_hash = handler.hash("stub") + self.assertFalse(handler.needs_update(new_hash)) + +# create test cases for specific backends +bsdi_crypt_os_crypt_test = _bsdi_crypt_test.create_backend_case("os_crypt") +bsdi_crypt_builtin_test = _bsdi_crypt_test.create_backend_case("builtin") + +#============================================================================= +# crypt16 +#============================================================================= +class crypt16_test(HandlerCase): + handler = hash.crypt16 + + # TODO: find an authortative source of test vectors + known_correct_hashes = [ + # + # from messages around the web, including + # http://seclists.org/bugtraq/1999/Mar/76 + # + ("passphrase", "qi8H8R7OM4xMUNMPuRAZxlY."), + ("printf", "aaCjFz4Sh8Eg2QSqAReePlq6"), + ("printf", "AA/xje2RyeiSU0iBY3PDwjYo"), + ("LOLOAQICI82QB4IP", "/.FcK3mad6JwYt8LVmDqz9Lc"), + ("LOLOAQICI", "/.FcK3mad6JwYSaRHJoTPzY2"), + ("LOLOAQIC", "/.FcK3mad6JwYelhbtlysKy6"), + ("L", "/.CIu/PzYCkl6elhbtlysKy6"), + + # + # custom + # + + # ensures utf-8 used for unicode + (UPASS_TABLE, 'YeDc9tKkkmDvwP7buzpwhoqQ'), + ] + +#============================================================================= +# des crypt +#============================================================================= +class _des_crypt_test(HandlerCase): + """test des-crypt algorithm""" + handler = hash.des_crypt + + known_correct_hashes = [ + # + # from JTR 1.7.9 + # + ('U*U*U*U*', 'CCNf8Sbh3HDfQ'), + ('U*U***U', 'CCX.K.MFy4Ois'), + ('U*U***U*', 'CC4rMpbg9AMZ.'), + ('*U*U*U*U', 'XXxzOu6maQKqQ'), + ('', 'SDbsugeBiC58A'), + + # + # custom + # + ('', 'OgAwTx2l6NADI'), + (' ', '/Hk.VPuwQTXbc'), + ('test', 'N1tQbOFcM5fpg'), + ('Compl3X AlphaNu3meric', 'um.Wguz3eVCx2'), + ('4lpHa N|_|M3r1K W/ Cur5Es: #$%(*)(*%#', 'sNYqfOyauIyic'), + ('AlOtBsOl', 'cEpWz5IUCShqM'), + + # ensures utf-8 used for unicode + (u('hell\u00D6'), 'saykDgk3BPZ9E'), + ] + known_unidentified_hashes = [ + # bad char in otherwise correctly formatted hash + #\/ + '!gAwTx2l6NADI', + + # wrong size + 'OgAwTx2l6NAD', + 'OgAwTx2l6NADIj', + ] + + platform_crypt_support = [ + # openbsd 5.8 dropped everything except bcrypt + ("openbsd[6789]", False), + ("openbsd5", None), + ("openbsd", True), + + ("freebsd|netbsd|linux|solaris|darwin", True), + ] + +# create test cases for specific backends +des_crypt_os_crypt_test = _des_crypt_test.create_backend_case("os_crypt") +des_crypt_builtin_test = _des_crypt_test.create_backend_case("builtin") + +#============================================================================= +# fshp +#============================================================================= +class fshp_test(HandlerCase): + """test fshp algorithm""" + handler = hash.fshp + + known_correct_hashes = [ + # + # test vectors from FSHP reference implementation + # https://github.com/bdd/fshp-is-not-secure-anymore/blob/master/python/test.py + # + ('test', '{FSHP0|0|1}qUqP5cyxm6YcTAhz05Hph5gvu9M='), + + ('test', + '{FSHP1|8|4096}MTIzNDU2NzjTdHcmoXwNc0f' + 'f9+ArUHoN0CvlbPZpxFi1C6RDM/MHSA==' + ), + + ('OrpheanBeholderScryDoubt', + '{FSHP1|8|4096}GVSUFDAjdh0vBosn1GUhz' + 'GLHP7BmkbCZVH/3TQqGIjADXpc+6NCg3g==' + ), + ('ExecuteOrder66', + '{FSHP3|16|8192}0aY7rZQ+/PR+Rd5/I9ss' + 'RM7cjguyT8ibypNaSp/U1uziNO3BVlg5qPU' + 'ng+zHUDQC3ao/JbzOnIBUtAeWHEy7a2vZeZ' + '7jAwyJJa2EqOsq4Io=' + ), + + # + # custom + # + + # ensures utf-8 used for unicode + (UPASS_TABLE, '{FSHP1|16|16384}9v6/l3Lu/d9by5nznpOS' + 'cqQo8eKu/b/CKli3RCkgYg4nRTgZu5y659YV8cCZ68UL'), + ] + + known_unidentified_hashes = [ + # incorrect header + '{FSHX0|0|1}qUqP5cyxm6YcTAhz05Hph5gvu9M=', + 'FSHP0|0|1}qUqP5cyxm6YcTAhz05Hph5gvu9M=', + ] + + known_malformed_hashes = [ + # bad base64 padding + '{FSHP0|0|1}qUqP5cyxm6YcTAhz05Hph5gvu9M', + + # wrong salt size + '{FSHP0|1|1}qUqP5cyxm6YcTAhz05Hph5gvu9M=', + + # bad rounds + '{FSHP0|0|A}qUqP5cyxm6YcTAhz05Hph5gvu9M=', + ] + + def test_90_variant(self): + """test variant keyword""" + handler = self.handler + kwds = dict(salt=b'a', rounds=1) + + # accepts ints + handler(variant=1, **kwds) + + # accepts bytes or unicode + handler(variant=u('1'), **kwds) + handler(variant=b'1', **kwds) + + # aliases + handler(variant=u('sha256'), **kwds) + handler(variant=b'sha256', **kwds) + + # rejects None + self.assertRaises(TypeError, handler, variant=None, **kwds) + + # rejects other types + self.assertRaises(TypeError, handler, variant=complex(1,1), **kwds) + + # invalid variant + self.assertRaises(ValueError, handler, variant='9', **kwds) + self.assertRaises(ValueError, handler, variant=9, **kwds) + +#============================================================================= +# hex digests +#============================================================================= +class hex_md4_test(HandlerCase): + handler = hash.hex_md4 + known_correct_hashes = [ + ("password", '8a9d093f14f8701df17732b2bb182c74'), + (UPASS_TABLE, '876078368c47817ce5f9115f3a42cf74'), + ] + +class hex_md5_test(HandlerCase): + handler = hash.hex_md5 + known_correct_hashes = [ + ("password", '5f4dcc3b5aa765d61d8327deb882cf99'), + (UPASS_TABLE, '05473f8a19f66815e737b33264a0d0b0'), + ] + + # XXX: should test this for ALL the create_hex_md5() hashers. + def test_mock_fips_mode(self): + """ + if md5 isn't available, a dummy instance should be created. + (helps on FIPS systems). + """ + from passlib.exc import UnknownHashError + from passlib.crypto.digest import lookup_hash, _set_mock_fips_mode + + # check if md5 is available so we can test mock helper + supported = lookup_hash("md5", required=False).supported + self.assertEqual(self.handler.supported, supported) + if supported: + _set_mock_fips_mode() + self.addCleanup(_set_mock_fips_mode, False) + + # HACK: have to recreate hasher, since underlying HashInfo has changed. + # could reload module and re-import, but this should be good enough. + from passlib.handlers.digests import create_hex_hash + hasher = create_hex_hash("md5", required=False) + self.assertFalse(hasher.supported) + + # can identify hashes even if disabled + ref1 = '5f4dcc3b5aa765d61d8327deb882cf99' + ref2 = 'xxx' + self.assertTrue(hasher.identify(ref1)) + self.assertFalse(hasher.identify(ref2)) + + # throw error if try to use it + pat = "'md5' hash disabled for fips" + self.assertRaisesRegex(UnknownHashError, pat, hasher.hash, "password") + self.assertRaisesRegex(UnknownHashError, pat, hasher.verify, "password", ref1) + + +class hex_sha1_test(HandlerCase): + handler = hash.hex_sha1 + known_correct_hashes = [ + ("password", '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8'), + (UPASS_TABLE, 'e059b2628e3a3e2de095679de9822c1d1466e0f0'), + ] + +class hex_sha256_test(HandlerCase): + handler = hash.hex_sha256 + known_correct_hashes = [ + ("password", '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8'), + (UPASS_TABLE, '6ed729e19bf24d3d20f564375820819932029df05547116cfc2cc868a27b4493'), + ] + +class hex_sha512_test(HandlerCase): + handler = hash.hex_sha512 + known_correct_hashes = [ + ("password", 'b109f3bbbc244eb82441917ed06d618b9008dd09b3befd1b5e07394c' + '706a8bb980b1d7785e5976ec049b46df5f1326af5a2ea6d103fd07c95385ffab0cac' + 'bc86'), + (UPASS_TABLE, 'd91bb0a23d66dca07a1781fd63ae6a05f6919ee5fc368049f350c9f' + '293b078a18165d66097cf0d89fdfbeed1ad6e7dba2344e57348cd6d51308c843a06f' + '29caf'), + ] + +#============================================================================= +# htdigest hash +#============================================================================= +class htdigest_test(UserHandlerMixin, HandlerCase): + handler = hash.htdigest + + known_correct_hashes = [ + # secret, user, realm + + # from RFC 2617 + (("Circle Of Life", "Mufasa", "testrealm@host.com"), + '939e7578ed9e3c518a452acee763bce9'), + + # custom + ((UPASS_TABLE, UPASS_USD, UPASS_WAV), + '4dabed2727d583178777fab468dd1f17'), + ] + + known_unidentified_hashes = [ + # bad char \/ - currently rejecting upper hex chars, may change + '939e7578edAe3c518a452acee763bce9', + + # bad char \/ + '939e7578edxe3c518a452acee763bce9', + ] + + def test_80_user(self): + raise self.skipTest("test case doesn't support 'realm' keyword") + + def populate_context(self, secret, kwds): + """insert username into kwds""" + if isinstance(secret, tuple): + secret, user, realm = secret + else: + user, realm = "user", "realm" + kwds.setdefault("user", user) + kwds.setdefault("realm", realm) + return secret + +#============================================================================= +# ldap hashes +#============================================================================= +class ldap_md5_test(HandlerCase): + handler = hash.ldap_md5 + known_correct_hashes = [ + ("helloworld", '{MD5}/F4DjTilcDIIVEHn/nAQsA=='), + (UPASS_TABLE, '{MD5}BUc/ihn2aBXnN7MyZKDQsA=='), + ] + +class ldap_sha1_test(HandlerCase): + handler = hash.ldap_sha1 + known_correct_hashes = [ + ("helloworld", '{SHA}at+xg6SiyUovktq1redipHiJpaE='), + (UPASS_TABLE, '{SHA}4FmyYo46Pi3glWed6YIsHRRm4PA='), + ] + +class ldap_salted_md5_test(HandlerCase): + handler = hash.ldap_salted_md5 + known_correct_hashes = [ + ("testing1234", '{SMD5}UjFY34os/pnZQ3oQOzjqGu4yeXE='), + (UPASS_TABLE, '{SMD5}Z0ioJ58LlzUeRxm3K6JPGAvBGIM='), + + # alternate salt sizes (8, 15, 16) + ('test', '{SMD5}LnuZPJhiaY95/4lmVFpg548xBsD4P4cw'), + ('test', '{SMD5}XRlncfRzvGi0FDzgR98tUgBg7B3jXOs9p9S615qTkg=='), + ('test', '{SMD5}FbAkzOMOxRbMp6Nn4hnZuel9j9Gas7a2lvI+x5hT6j0='), + ] + + known_malformed_hashes = [ + # salt too small (3) + '{SMD5}IGVhwK+anvspmfDt2t0vgGjt/Q==', + + # incorrect base64 encoding + '{SMD5}LnuZPJhiaY95/4lmVFpg548xBsD4P4c', + '{SMD5}LnuZPJhiaY95/4lmVFpg548xBsD4P4cw' + '{SMD5}LnuZPJhiaY95/4lmVFpg548xBsD4P4cw=', + '{SMD5}LnuZPJhiaY95/4lmV=pg548xBsD4P4cw', + '{SMD5}LnuZPJhiaY95/4lmVFpg548xBsD4P===', + ] + +class ldap_salted_sha1_test(HandlerCase): + handler = hash.ldap_salted_sha1 + known_correct_hashes = [ + ("testing123", '{SSHA}0c0blFTXXNuAMHECS4uxrj3ZieMoWImr'), + ("secret", "{SSHA}0H+zTv8o4MR4H43n03eCsvw1luG8LdB7"), + (UPASS_TABLE, '{SSHA}3yCSD1nLZXznra4N8XzZgAL+s1sQYsx5'), + + # alternate salt sizes (8, 15, 16) + ('test', '{SSHA}P90+qijSp8MJ1tN25j5o1PflUvlqjXHOGeOckw=='), + ('test', '{SSHA}/ZMF5KymNM+uEOjW+9STKlfCFj51bg3BmBNCiPHeW2ttbU0='), + ('test', '{SSHA}Pfx6Vf48AT9x3FVv8znbo8WQkEVSipHSWovxXmvNWUvp/d/7'), + ] + + known_malformed_hashes = [ + # salt too small (3) + '{SSHA}ZQK3Yvtvl6wtIRoISgMGPkcWU7Nfq5U=', + + # incorrect base64 encoding + '{SSHA}P90+qijSp8MJ1tN25j5o1PflUvlqjXHOGeOck', + '{SSHA}P90+qijSp8MJ1tN25j5o1PflUvlqjXHOGeOckw=', + '{SSHA}P90+qijSp8MJ1tN25j5o1Pf=UvlqjXHOGeOckw==', + '{SSHA}P90+qijSp8MJ1tN25j5o1PflUvlqjXHOGeOck===', + ] + + +class ldap_salted_sha256_test(HandlerCase): + handler = hash.ldap_salted_sha256 + known_correct_hashes = [ + # generated locally + # salt size = 8 + ("password", '{SSHA256}x1tymSTVjozxQ2PtT46ysrzhZxbcskK0o2f8hEFx7fAQQmhtDSEkJA=='), + ("test", '{SSHA256}xfqc9aOR6z15YaEk3/Ufd7UL9+JozB/1EPmCDTizL0GkdA7BuNda6w=='), + ("toomanysecrets", '{SSHA256}RrTKrg6HFXcjJ+eDAq4UtbODxOr9RLeG+I69FoJvutcbY0zpfU+p1Q=='), + (u('letm\xe8\xefn'), '{SSHA256}km7UjUTBZN8a+gf1ND2/qn15N7LsO/jmGYJXvyTfJKAbI0RoLWWslQ=='), + + # alternate salt sizes (4, 15, 16) + # generated locally + ('test', '{SSHA256}TFv2RpwyO0U9mA0Hk8FsXRa1I+4dNUtv27Qa8dzGVLinlDIm'), + ('test', '{SSHA256}J6MFQdkfjdmXz9UyUPb773kekJdm4dgSL4y8WQEQW11VipHSundOKaV0LsV4L6U='), + ('test', '{SSHA256}uBLazLaiBaPb6Cpnvq2XTYDkvXbYIuqRW1anMKk85d1/j1GqFQIgpHSOMUYIIcS4'), + ] + + known_malformed_hashes = [ + # salt too small (3) + '{SSHA256}Lpdyr1+lR+rtxgp3SpQnUuNw33ENivTl28nzF2ZI4Gm41/o=', + + # incorrect base64 encoding + '{SSHA256}TFv2RpwyO0U9mA0Hk8FsXRa1I+4dNUtv27Qa8dzGVLinlDI@', + '{SSHA256}TFv2RpwyO0U9mA0Hk8FsXRa1I+4dNUtv27Qa8dzGVLinlDI', + '{SSHA256}TFv2RpwyO0U9mA0Hk8FsXRa1I+4dNUtv27Qa8dzGVLinlDIm===', + ] + + + +class ldap_salted_sha512_test(HandlerCase): + handler = hash.ldap_salted_sha512 + known_correct_hashes = [ + # generated by testing ldap server web interface (see issue 124 comments) + # salt size = 8 + ("toomanysecrets", '{SSHA512}wExp4xjiCHS0zidJDC4UJq9EEeIebAQPJ1PWSwfhxWjfutI9XiiKuHm2AE41cEFfK+8HyI8bh+ztbczUGsvVFIgICWWPt7qu'), + (u('letm\xe8\xefn'), '{SSHA512}mpNUSmZc3TNx+RnPwkIAVMf7ocEKLPrIoQNsg4Eu8dHvyCeb2xzHp5A6n4tF7ntknSvfvRZaJII4ImvNJlYsgiwAm0FMqR+3'), + + # generated locally + # salt size = 8 + ("password", '{SSHA512}f/lFQskkl7PdMsTGJxHZq8LDt/l+UqRMm6/pj4pV7/xZkcOaKCgvQqp+KCeXc/Vd4RY6vEHWn4y0DnFcQ6wgyv9fyxk='), + ("test", '{SSHA512}Tgx/uhHnlM9/GgQvI31dN7cheDXg7WypZwaaIkyRsgV/BKIzBG3G/wUd9o1dpi06p3SYzMedg0lvTc3b6CtdO0Xo/f9/L+Uc'), + + # alternate salt sizes (4, 15, 16) + # generated locally + ('test', '{SSHA512}Yg9DQ2wURCFGwobu7R2O6cq7nVbnGMPrFCX0aPQ9kj/y1hd6k9PEzkgWCB5aXdPwPzNrVb0PkiHiBnG1CxFiT+B8L8U='), + ('test', '{SSHA512}5ecDGWs5RY4xLszUO6hAcl90W3wAozGQoI4Gqj8xSZdcfU1lVEM4aY8s+4xVeLitcn7BO8i7xkzMFWLoxas7SeHc23sP4dx77937PyeE0A=='), + ('test', '{SSHA512}6FQv5W47HGg2MFBFZofoiIbO8KRW75Pm51NKoInpthYQQ5ujazHGhVGzrj3JXgA7j0k+UNmkHdbJjdY5xcUHPzynFEII4fwfIySEcG5NKSU='), + ] + + known_malformed_hashes = [ + # salt too small (3) + '{SSHA512}zFnn4/8x8GveUaMqgrYWyIWqFQ0Irt6gADPtRk4Uv3nUC6uR5cD8+YdQni/0ZNij9etm6p17kSFuww3M6l+d6AbAeA==', + + # incorrect base64 encoding + '{SSHA512}Tgx/uhHnlM9/GgQvI31dN7cheDXg7WypZwaaIkyRsgV/BKIzBG3G/wUd9o1dpi06p3SYzMedg0lvTc3b6CtdO0Xo/f9/L+U', + '{SSHA512}Tgx/uhHnlM9/GgQvI31dN7cheDXg7WypZwaaIkyRsgV/BKIzBG3G/wUd9o1dpi06p3SYzMedg0lvTc3b6CtdO0Xo/f9/L+U@', + '{SSHA512}Tgx/uhHnlM9/GgQvI31dN7cheDXg7WypZwaaIkyRsgV/BKIzBG3G/wUd9o1dpi06p3SYzMedg0lvTc3b6CtdO0Xo/f9/L+U===', + ] + + +class ldap_plaintext_test(HandlerCase): + # TODO: integrate EncodingHandlerMixin + handler = hash.ldap_plaintext + known_correct_hashes = [ + ("password", 'password'), + (UPASS_TABLE, UPASS_TABLE if PY3 else PASS_TABLE_UTF8), + (PASS_TABLE_UTF8, UPASS_TABLE if PY3 else PASS_TABLE_UTF8), + ] + known_unidentified_hashes = [ + "{FOO}bar", + + # NOTE: this hash currently rejects the empty string. + "", + ] + + known_other_hashes = [ + ("ldap_md5", "{MD5}/F4DjTilcDIIVEHn/nAQsA==") + ] + + class FuzzHashGenerator(HandlerCase.FuzzHashGenerator): + + def random_password(self): + # NOTE: this hash currently rejects the empty string. + while True: + pwd = super(ldap_plaintext_test.FuzzHashGenerator, self).random_password() + if pwd: + return pwd + +class _ldap_md5_crypt_test(HandlerCase): + # NOTE: since the ldap_{crypt} handlers are all wrappers, don't need + # separate test; this is just to test the codebase end-to-end + handler = hash.ldap_md5_crypt + + known_correct_hashes = [ + # + # custom + # + ('', '{CRYPT}$1$dOHYPKoP$tnxS1T8Q6VVn3kpV8cN6o.'), + (' ', '{CRYPT}$1$m/5ee7ol$bZn0kIBFipq39e.KDXX8I0'), + ('test', '{CRYPT}$1$ec6XvcoW$ghEtNK2U1MC5l.Dwgi3020'), + ('Compl3X AlphaNu3meric', '{CRYPT}$1$nX1e7EeI$ljQn72ZUgt6Wxd9hfvHdV0'), + ('4lpHa N|_|M3r1K W/ Cur5Es: #$%(*)(*%#', '{CRYPT}$1$jQS7o98J$V6iTcr71CGgwW2laf17pi1'), + ('test', '{CRYPT}$1$SuMrG47N$ymvzYjr7QcEQjaK5m1PGx1'), + + # ensures utf-8 used for unicode + (UPASS_TABLE, '{CRYPT}$1$d6/Ky1lU$/xpf8m7ftmWLF.TjHCqel0'), + ] + + known_malformed_hashes = [ + # bad char in otherwise correct hash + '{CRYPT}$1$dOHYPKoP$tnxS1T8Q6VVn3kpV8cN6o!', + ] + +# create test cases for specific backends +ldap_md5_crypt_os_crypt_test =_ldap_md5_crypt_test.create_backend_case("os_crypt") +ldap_md5_crypt_builtin_test =_ldap_md5_crypt_test.create_backend_case("builtin") + +class _ldap_sha1_crypt_test(HandlerCase): + # NOTE: this isn't for testing the hash (see ldap_md5_crypt note) + # but as a self-test of the os_crypt patching code in HandlerCase. + handler = hash.ldap_sha1_crypt + + known_correct_hashes = [ + ('password', '{CRYPT}$sha1$10$c.mcTzCw$gF8UeYst9yXX7WNZKc5Fjkq0.au7'), + (UPASS_TABLE, '{CRYPT}$sha1$10$rnqXlOsF$aGJf.cdRPewJAXo1Rn1BkbaYh0fP'), + ] + + def populate_settings(self, kwds): + kwds.setdefault("rounds", 10) + super(_ldap_sha1_crypt_test, self).populate_settings(kwds) + + def test_77_fuzz_input(self, **ignored): + raise self.skipTest("unneeded") + +# create test cases for specific backends +ldap_sha1_crypt_os_crypt_test = _ldap_sha1_crypt_test.create_backend_case("os_crypt") + +#============================================================================= +# lanman +#============================================================================= +class lmhash_test(EncodingHandlerMixin, HandlerCase): + handler = hash.lmhash + secret_case_insensitive = True + + known_correct_hashes = [ + # + # http://msdn.microsoft.com/en-us/library/cc245828(v=prot.10).aspx + # + ("OLDPASSWORD", "c9b81d939d6fd80cd408e6b105741864"), + ("NEWPASSWORD", '09eeab5aa415d6e4d408e6b105741864'), + ("welcome", "c23413a8a1e7665faad3b435b51404ee"), + + # + # custom + # + ('', 'aad3b435b51404eeaad3b435b51404ee'), + ('zzZZZzz', 'a5e6066de61c3e35aad3b435b51404ee'), + ('passphrase', '855c3697d9979e78ac404c4ba2c66533'), + ('Yokohama', '5ecd9236d21095ce7584248b8d2c9f9e'), + + # ensures cp437 used for unicode + (u('ENCYCLOP\xC6DIA'), 'fed6416bffc9750d48462b9d7aaac065'), + (u('encyclop\xE6dia'), 'fed6416bffc9750d48462b9d7aaac065'), + + # test various encoding values + ((u("\xC6"), None), '25d8ab4a0659c97aaad3b435b51404ee'), + ((u("\xC6"), "cp437"), '25d8ab4a0659c97aaad3b435b51404ee'), + ((u("\xC6"), "latin-1"), '184eecbbe9991b44aad3b435b51404ee'), + ((u("\xC6"), "utf-8"), '00dd240fcfab20b8aad3b435b51404ee'), + ] + + known_unidentified_hashes = [ + # bad char in otherwise correct hash + '855c3697d9979e78ac404c4ba2c6653X', + ] + + def test_90_raw(self): + """test lmhash.raw() method""" + from binascii import unhexlify + from passlib.utils.compat import str_to_bascii + lmhash = self.handler + for secret, hash in self.known_correct_hashes: + kwds = {} + secret = self.populate_context(secret, kwds) + data = unhexlify(str_to_bascii(hash)) + self.assertEqual(lmhash.raw(secret, **kwds), data) + self.assertRaises(TypeError, lmhash.raw, 1) + +#============================================================================= +# md5 crypt +#============================================================================= +class _md5_crypt_test(HandlerCase): + handler = hash.md5_crypt + + known_correct_hashes = [ + # + # from JTR 1.7.9 + # + ('U*U*U*U*', '$1$dXc3I7Rw$ctlgjDdWJLMT.qwHsWhXR1'), + ('U*U***U', '$1$dXc3I7Rw$94JPyQc/eAgQ3MFMCoMF.0'), + ('U*U***U*', '$1$dXc3I7Rw$is1mVIAEtAhIzSdfn5JOO0'), + ('*U*U*U*U', '$1$eQT9Hwbt$XtuElNJD.eW5MN5UCWyTQ0'), + ('', '$1$Eu.GHtia$CFkL/nE1BYTlEPiVx1VWX0'), + + # + # custom + # + + # NOTE: would need to patch HandlerCase to coerce hashes + # to native str for this first one to work under py3. +## ('', b('$1$dOHYPKoP$tnxS1T8Q6VVn3kpV8cN6o.')), + ('', '$1$dOHYPKoP$tnxS1T8Q6VVn3kpV8cN6o.'), + (' ', '$1$m/5ee7ol$bZn0kIBFipq39e.KDXX8I0'), + ('test', '$1$ec6XvcoW$ghEtNK2U1MC5l.Dwgi3020'), + ('Compl3X AlphaNu3meric', '$1$nX1e7EeI$ljQn72ZUgt6Wxd9hfvHdV0'), + ('4lpHa N|_|M3r1K W/ Cur5Es: #$%(*)(*%#', '$1$jQS7o98J$V6iTcr71CGgwW2laf17pi1'), + ('test', '$1$SuMrG47N$ymvzYjr7QcEQjaK5m1PGx1'), + (b'test', '$1$SuMrG47N$ymvzYjr7QcEQjaK5m1PGx1'), + (u('s'), '$1$ssssssss$YgmLTApYTv12qgTwBoj8i/'), + + # ensures utf-8 used for unicode + (UPASS_TABLE, '$1$d6/Ky1lU$/xpf8m7ftmWLF.TjHCqel0'), + ] + + known_malformed_hashes = [ + # bad char in otherwise correct hash \/ + '$1$dOHYPKoP$tnxS1T8Q6VVn3kpV8cN6o!', + + # too many fields + '$1$dOHYPKoP$tnxS1T8Q6VVn3kpV8cN6o.$', + ] + + platform_crypt_support = [ + # openbsd 5.8 dropped everything except bcrypt + ("openbsd[6789]", False), + ("openbsd5", None), + ("openbsd", True), + + ("freebsd|netbsd|linux|solaris", True), + ("darwin", False), + ] + +# create test cases for specific backends +md5_crypt_os_crypt_test = _md5_crypt_test.create_backend_case("os_crypt") +md5_crypt_builtin_test = _md5_crypt_test.create_backend_case("builtin") + +#============================================================================= +# msdcc 1 & 2 +#============================================================================= +class msdcc_test(UserHandlerMixin, HandlerCase): + handler = hash.msdcc + user_case_insensitive = True + + known_correct_hashes = [ + + # + # http://www.jedge.com/wordpress/windows-password-cache/ + # + (("Asdf999", "sevans"), "b1176c2587478785ec1037e5abc916d0"), + + # + # http://infosecisland.com/blogview/12156-Cachedump-for-Meterpreter-in-Action.html + # + (("ASDqwe123", "jdoe"), "592cdfbc3f1ef77ae95c75f851e37166"), + + # + # http://comments.gmane.org/gmane.comp.security.openwall.john.user/1917 + # + (("test1", "test1"), "64cd29e36a8431a2b111378564a10631"), + (("test2", "test2"), "ab60bdb4493822b175486810ac2abe63"), + (("test3", "test3"), "14dd041848e12fc48c0aa7a416a4a00c"), + (("test4", "test4"), "b945d24866af4b01a6d89b9d932a153c"), + + # + # http://ciscoit.wordpress.com/2011/04/13/metasploit-hashdump-vs-cachedump/ + # + (("1234qwer!@#$", "Administrator"), "7b69d06ef494621e3f47b9802fe7776d"), + + # + # http://www.securiteam.com/tools/5JP0I2KFPA.html + # + (("password", "user"), "2d9f0b052932ad18b87f315641921cda"), + + # + # from JTR 1.7.9 + # + (("", "root"), "176a4c2bd45ac73687676c2f09045353"), + (("test1", "TEST1"), "64cd29e36a8431a2b111378564a10631"), + (("okolada", "nineteen_characters"), "290efa10307e36a79b3eebf2a6b29455"), + ((u("\u00FC"), u("\u00FC")), "48f84e6f73d6d5305f6558a33fa2c9bb"), + ((u("\u00FC\u00FC"), u("\u00FC\u00FC")), "593246a8335cf0261799bda2a2a9c623"), + ((u("\u20AC\u20AC"), "user"), "9121790702dda0fa5d353014c334c2ce"), + + # + # custom + # + + # ensures utf-8 used for unicode + ((UPASS_TABLE, 'bob'), 'fcb82eb4212865c7ac3503156ca3f349'), + ] + + known_alternate_hashes = [ + # check uppercase accepted. + ("B1176C2587478785EC1037E5ABC916D0", ("Asdf999", "sevans"), + "b1176c2587478785ec1037e5abc916d0"), + ] + +class msdcc2_test(UserHandlerMixin, HandlerCase): + handler = hash.msdcc2 + user_case_insensitive = True + + known_correct_hashes = [ + # + # from JTR 1.7.9 + # + (("test1", "test1"), "607bbe89611e37446e736f7856515bf8"), + (("qerwt", "Joe"), "e09b38f84ab0be586b730baf61781e30"), + (("12345", "Joe"), "6432f517a900b3fc34ffe57f0f346e16"), + (("", "bin"), "c0cbe0313a861062e29f92ede58f9b36"), + (("w00t", "nineteen_characters"), "87136ae0a18b2dafe4a41d555425b2ed"), + (("w00t", "eighteencharacters"), "fc5df74eca97afd7cd5abb0032496223"), + (("longpassword", "twentyXXX_characters"), "cfc6a1e33eb36c3d4f84e4c2606623d2"), + (("longpassword", "twentyoneX_characters"), "99ff74cea552799da8769d30b2684bee"), + (("longpassword", "twentytwoXX_characters"), "0a721bdc92f27d7fb23b87a445ec562f"), + (("test2", "TEST2"), "c6758e5be7fc943d00b97972a8a97620"), + (("test3", "test3"), "360e51304a2d383ea33467ab0b639cc4"), + (("test4", "test4"), "6f79ee93518306f071c47185998566ae"), + ((u("\u00FC"), "joe"), "bdb80f2c4656a8b8591bd27d39064a54"), + ((u("\u20AC\u20AC"), "joe"), "1e1e20f482ff748038e47d801d0d1bda"), + ((u("\u00FC\u00FC"), "admin"), "0839e4a07c00f18a8c65cf5b985b9e73"), + + # + # custom + # + + # custom unicode test + ((UPASS_TABLE, 'bob'), 'cad511dc9edefcf69201da72efb6bb55'), + ] + +#============================================================================= +# mssql 2000 & 2005 +#============================================================================= +class mssql2000_test(HandlerCase): + handler = hash.mssql2000 + secret_case_insensitive = "verify-only" + # FIXME: fix UT framework - this hash is sensitive to password case, but verify() is not + + known_correct_hashes = [ + # + # http://hkashfi.blogspot.com/2007/08/breaking-sql-server-2005-hashes.html + # + ('Test', '0x010034767D5C0CFA5FDCA28C4A56085E65E882E71CB0ED2503412FD54D6119FFF04129A1D72E7C3194F7284A7F3A'), + ('TEST', '0x010034767D5C2FD54D6119FFF04129A1D72E7C3194F7284A7F3A2FD54D6119FFF04129A1D72E7C3194F7284A7F3A'), + + # + # http://www.sqlmag.com/forums/aft/68438 + # + ('x', '0x010086489146C46DD7318D2514D1AC706457CBF6CD3DF8407F071DB4BBC213939D484BF7A766E974F03C96524794'), + + # + # http://stackoverflow.com/questions/173329/how-to-decrypt-a-password-from-sql-server + # + ('AAAA', '0x0100CF465B7B12625EF019E157120D58DD46569AC7BF4118455D12625EF019E157120D58DD46569AC7BF4118455D'), + + # + # http://msmvps.com/blogs/gladchenko/archive/2005/04/06/41083.aspx + # + ('123', '0x01002D60BA07FE612C8DE537DF3BFCFA49CD9968324481C1A8A8FE612C8DE537DF3BFCFA49CD9968324481C1A8A8'), + + # + # http://www.simple-talk.com/sql/t-sql-programming/temporarily-changing-an-unknown-password-of-the-sa-account-/ + # + ('12345', '0x01005B20054332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B3'), + + # + # XXX: sample is incomplete, password unknown + # https://anthonystechblog.wordpress.com/2011/04/20/password-encryption-in-sql-server-how-to-tell-if-a-user-is-using-a-weak-password/ + # (????, '0x0100813F782D66EF15E40B1A3FDF7AB88B322F51401A87D8D3E3A8483C4351A3D96FC38499E6CDD2B6F?????????'), + # + + # + # from JTR 1.7.9 + # + ('foo', '0x0100A607BA7C54A24D17B565C59F1743776A10250F581D482DA8B6D6261460D3F53B279CC6913CE747006A2E3254'), + ('bar', '0x01000508513EADDF6DB7DDD270CCA288BF097F2FF69CC2DB74FBB9644D6901764F999BAB9ECB80DE578D92E3F80D'), + ('canard', '0x01008408C523CF06DCB237835D701C165E68F9460580132E28ED8BC558D22CEDF8801F4503468A80F9C52A12C0A3'), + ('lapin', '0x0100BF088517935FC9183FE39FDEC77539FD5CB52BA5F5761881E5B9638641A79DBF0F1501647EC941F3355440A2'), + + # + # custom + # + + # ensures utf-8 used for unicode + (UPASS_USD, '0x0100624C0961B28E39FEE13FD0C35F57B4523F0DA1861C11D5A5B28E39FEE13FD0C35F57B4523F0DA1861C11D5A5'), + (UPASS_TABLE, '0x010083104228FAD559BE52477F2131E538BE9734E5C4B0ADEFD7F6D784B03C98585DC634FE2B8CA3A6DFFEC729B4'), + + ] + + known_alternate_hashes = [ + # lower case hex + ('0x01005b20054332752e1bc2e7c5df0f9ebfe486e9bee063e8d3b332752e1bc2e7c5df0f9ebfe486e9bee063e8d3b3', + '12345', '0x01005B20054332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B3'), + ] + + known_unidentified_hashes = [ + # malformed start + '0X01005B20054332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B3', + + # wrong magic value + '0x02005B20054332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B3', + + # wrong size + '0x01005B20054332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3', + '0x01005B20054332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B3AF', + + # mssql2005 + '0x01005B20054332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B3', + ] + + known_malformed_hashes = [ + # non-hex char -----\/ + b'0x01005B200543327G2E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B3', + u('0x01005B200543327G2E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B3'), + ] + +class mssql2005_test(HandlerCase): + handler = hash.mssql2005 + + known_correct_hashes = [ + # + # http://hkashfi.blogspot.com/2007/08/breaking-sql-server-2005-hashes.html + # + ('TEST', '0x010034767D5C2FD54D6119FFF04129A1D72E7C3194F7284A7F3A'), + + # + # http://www.openwall.com/lists/john-users/2009/07/14/2 + # + ('toto', '0x01004086CEB6BF932BC4151A1AF1F13CD17301D70816A8886908'), + + # + # http://msmvps.com/blogs/gladchenko/archive/2005/04/06/41083.aspx + # + ('123', '0x01004A335DCEDB366D99F564D460B1965B146D6184E4E1025195'), + ('123', '0x0100E11D573F359629B344990DCD3D53DE82CF8AD6BBA7B638B6'), + + # + # XXX: password unknown + # http://www.simple-talk.com/sql/t-sql-programming/temporarily-changing-an-unknown-password-of-the-sa-account-/ + # (???, '0x01004086CEB6301EEC0A994E49E30DA235880057410264030797'), + # + + # + # http://therelentlessfrontend.com/2010/03/26/encrypting-and-decrypting-passwords-in-sql-server/ + # + ('AAAA', '0x010036D726AE86834E97F20B198ACD219D60B446AC5E48C54F30'), + + # + # from JTR 1.7.9 + # + ("toto", "0x01004086CEB6BF932BC4151A1AF1F13CD17301D70816A8886908"), + ("titi", "0x01004086CEB60ED526885801C23B366965586A43D3DEAC6DD3FD"), + ("foo", "0x0100A607BA7C54A24D17B565C59F1743776A10250F581D482DA8"), + ("bar", "0x01000508513EADDF6DB7DDD270CCA288BF097F2FF69CC2DB74FB"), + ("canard", "0x01008408C523CF06DCB237835D701C165E68F9460580132E28ED"), + ("lapin", "0x0100BF088517935FC9183FE39FDEC77539FD5CB52BA5F5761881"), + + # + # adapted from mssql2000.known_correct_hashes (above) + # + ('Test', '0x010034767D5C0CFA5FDCA28C4A56085E65E882E71CB0ED250341'), + ('Test', '0x0100993BF2315F36CC441485B35C4D84687DC02C78B0E680411F'), + ('x', '0x010086489146C46DD7318D2514D1AC706457CBF6CD3DF8407F07'), + ('AAAA', '0x0100CF465B7B12625EF019E157120D58DD46569AC7BF4118455D'), + ('123', '0x01002D60BA07FE612C8DE537DF3BFCFA49CD9968324481C1A8A8'), + ('12345', '0x01005B20054332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B3'), + + # + # custom + # + + # ensures utf-8 used for unicode + (UPASS_USD, '0x0100624C0961B28E39FEE13FD0C35F57B4523F0DA1861C11D5A5'), + (UPASS_TABLE, '0x010083104228FAD559BE52477F2131E538BE9734E5C4B0ADEFD7'), + ] + + known_alternate_hashes = [ + # lower case hex + ('0x01005b20054332752e1bc2e7c5df0f9ebfe486e9bee063e8d3b3', + '12345', '0x01005B20054332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B3'), + ] + + known_unidentified_hashes = [ + # malformed start + '0X010036D726AE86834E97F20B198ACD219D60B446AC5E48C54F30', + + # wrong magic value + '0x020036D726AE86834E97F20B198ACD219D60B446AC5E48C54F30', + + # wrong size + '0x010036D726AE86834E97F20B198ACD219D60B446AC5E48C54F', + '0x010036D726AE86834E97F20B198ACD219D60B446AC5E48C54F3012', + + # mssql2000 + '0x01005B20054332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B3', + ] + + known_malformed_hashes = [ + # non-hex char --\/ + '0x010036D726AE86G34E97F20B198ACD219D60B446AC5E48C54F30', + ] + +#============================================================================= +# mysql 323 & 41 +#============================================================================= +class mysql323_test(HandlerCase): + handler = hash.mysql323 + + known_correct_hashes = [ + # + # from JTR 1.7.9 + # + ('drew', '697a7de87c5390b2'), + ('password', "5d2e19393cc5ef67"), + + # + # custom + # + ('mypass', '6f8c114b58f2ce9e'), + + # ensures utf-8 used for unicode + (UPASS_TABLE, '4ef327ca5491c8d7'), + ] + + known_unidentified_hashes = [ + # bad char in otherwise correct hash + '6z8c114b58f2ce9e', + ] + + def test_90_whitespace(self): + """check whitespace is ignored per spec""" + h = self.do_encrypt("mypass") + h2 = self.do_encrypt("my pass") + self.assertEqual(h, h2) + + class FuzzHashGenerator(HandlerCase.FuzzHashGenerator): + + def accept_password_pair(self, secret, other): + # override to handle whitespace + return secret.replace(" ","") != other.replace(" ","") + +class mysql41_test(HandlerCase): + handler = hash.mysql41 + known_correct_hashes = [ + # + # from JTR 1.7.9 + # + ('verysecretpassword', '*2C905879F74F28F8570989947D06A8429FB943E6'), + ('12345678123456781234567812345678', '*F9F1470004E888963FB466A5452C9CBD9DF6239C'), + ("' OR 1 /*'", '*97CF7A3ACBE0CA58D5391AC8377B5D9AC11D46D9'), + + # + # custom + # + ('mypass', '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF4'), + + # ensures utf-8 used for unicode + (UPASS_TABLE, '*E7AFE21A9CFA2FC9D15D942AE8FB5C240FE5837B'), + ] + known_unidentified_hashes = [ + # bad char in otherwise correct hash + '*6Z8989366EAF75BB670AD8EA7A7FC1176A95CEF4', + ] + +#============================================================================= +# NTHASH +#============================================================================= +class nthash_test(HandlerCase): + handler = hash.nthash + + known_correct_hashes = [ + # + # http://msdn.microsoft.com/en-us/library/cc245828(v=prot.10).aspx + # + ("OLDPASSWORD", u("6677b2c394311355b54f25eec5bfacf5")), + ("NEWPASSWORD", u("256781a62031289d3c2c98c14f1efc8c")), + + # + # from JTR 1.7.9 + # + + # ascii + ('', '31d6cfe0d16ae931b73c59d7e0c089c0'), + ('tigger', 'b7e0ea9fbffcf6dd83086e905089effd'), + + # utf-8 + (b'\xC3\xBC', '8bd6e4fb88e01009818749c5443ea712'), + (b'\xC3\xBC\xC3\xBC', 'cc1260adb6985ca749f150c7e0b22063'), + (b'\xE2\x82\xAC', '030926b781938db4365d46adc7cfbcb8'), + (b'\xE2\x82\xAC\xE2\x82\xAC','682467b963bb4e61943e170a04f7db46'), + + # + # custom + # + ('passphrase', '7f8fe03093cc84b267b109625f6bbf4b'), + ] + + known_unidentified_hashes = [ + # bad char in otherwise correct hash + '7f8fe03093cc84b267b109625f6bbfxb', + ] + +class bsd_nthash_test(HandlerCase): + handler = hash.bsd_nthash + + known_correct_hashes = [ + ('passphrase', '$3$$7f8fe03093cc84b267b109625f6bbf4b'), + (b'\xC3\xBC', '$3$$8bd6e4fb88e01009818749c5443ea712'), + ] + + known_unidentified_hashes = [ + # bad char in otherwise correct hash --\/ + '$3$$7f8fe03093cc84b267b109625f6bbfxb', + ] + +#============================================================================= +# oracle 10 & 11 +#============================================================================= +class oracle10_test(UserHandlerMixin, HandlerCase): + handler = hash.oracle10 + secret_case_insensitive = True + user_case_insensitive = True + + # TODO: get more test vectors (especially ones which properly test unicode) + known_correct_hashes = [ + # ((secret,user),hash) + + # + # http://www.petefinnigan.com/default/default_password_list.htm + # + (('tiger', 'scott'), 'F894844C34402B67'), + ((u('ttTiGGeR'), u('ScO')), '7AA1A84E31ED7771'), + (("d_syspw", "SYSTEM"), '1B9F1F9A5CB9EB31'), + (("strat_passwd", "strat_user"), 'AEBEDBB4EFB5225B'), + + # + # http://openwall.info/wiki/john/sample-hashes + # + (('#95LWEIGHTS', 'USER'), '000EA4D72A142E29'), + (('CIAO2010', 'ALFREDO'), 'EB026A76F0650F7B'), + + # + # from JTR 1.7.9 + # + (('GLOUGlou', 'Bob'), 'CDC6B483874B875B'), + (('GLOUGLOUTER', 'bOB'), 'EF1F9139DB2D5279'), + (('LONG_MOT_DE_PASSE_OUI', 'BOB'), 'EC8147ABB3373D53'), + + # + # custom + # + ((UPASS_TABLE, 'System'), 'B915A853F297B281'), + ] + + known_unidentified_hashes = [ + # bad char in hash --\ + 'F894844C34402B6Z', + ] + +class oracle11_test(HandlerCase): + handler = hash.oracle11 + # TODO: find more test vectors (especially ones which properly test unicode) + known_correct_hashes = [ + # + # from JTR 1.7.9 + # + ("abc123", "S:5FDAB69F543563582BA57894FE1C1361FB8ED57B903603F2C52ED1B4D642"), + ("SyStEm123!@#", "S:450F957ECBE075D2FA009BA822A9E28709FBC3DA82B44D284DDABEC14C42"), + ("oracle", "S:3437FF72BD69E3FB4D10C750B92B8FB90B155E26227B9AB62D94F54E5951"), + ("11g", "S:61CE616647A4F7980AFD7C7245261AF25E0AFE9C9763FCF0D54DA667D4E6"), + ("11g", "S:B9E7556F53500C8C78A58F50F24439D79962DE68117654B6700CE7CC71CF"), + + # + # source? + # + ("SHAlala", "S:2BFCFDF5895014EE9BB2B9BA067B01E0389BB5711B7B5F82B7235E9E182C"), + + # + # custom + # + (UPASS_TABLE, 'S:51586343E429A6DF024B8F242F2E9F8507B1096FACD422E29142AA4974B0'), + ] + +#============================================================================= +# PHPass Portable Crypt +#============================================================================= +class phpass_test(HandlerCase): + handler = hash.phpass + + known_correct_hashes = [ + # + # from official 0.3 implementation + # http://www.openwall.com/phpass/ + # + ('test12345', '$P$9IQRaTwmfeRo7ud9Fh4E2PdI0S3r.L0'), # from the source + + # + # from JTR 1.7.9 + # + ('test1', '$H$9aaaaaSXBjgypwqm.JsMssPLiS8YQ00'), + ('123456', '$H$9PE8jEklgZhgLmZl5.HYJAzfGCQtzi1'), + ('123456', '$H$9pdx7dbOW3Nnt32sikrjAxYFjX8XoK1'), + ('thisisalongertestPW', '$P$912345678LIjjb6PhecupozNBmDndU0'), + ('JohnRipper', '$P$612345678si5M0DDyPpmRCmcltU/YW/'), + ('JohnRipper', '$H$712345678WhEyvy1YWzT4647jzeOmo0'), + ('JohnRipper', '$P$B12345678L6Lpt4BxNotVIMILOa9u81'), + + # + # custom + # + ('', '$P$7JaFQsPzJSuenezefD/3jHgt5hVfNH0'), + ('compL3X!', '$P$FiS0N5L672xzQx1rt1vgdJQRYKnQM9/'), + + # ensures utf-8 used for unicode + (UPASS_TABLE, '$P$7SMy8VxnfsIy2Sxm7fJxDSdil.h7TW.'), + ] + + known_malformed_hashes = [ + # bad char in otherwise correct hash + # ---\/ + '$P$9IQRaTwmfeRo7ud9Fh4E2PdI0S3r!L0', + ] + +#============================================================================= +# plaintext +#============================================================================= +class plaintext_test(HandlerCase): + # TODO: integrate EncodingHandlerMixin + handler = hash.plaintext + accepts_all_hashes = True + + known_correct_hashes = [ + ('',''), + ('password', 'password'), + + # ensure unicode uses utf-8 + (UPASS_TABLE, UPASS_TABLE if PY3 else PASS_TABLE_UTF8), + (PASS_TABLE_UTF8, UPASS_TABLE if PY3 else PASS_TABLE_UTF8), + ] + +#============================================================================= +# postgres_md5 +#============================================================================= +class postgres_md5_test(UserHandlerMixin, HandlerCase): + handler = hash.postgres_md5 + known_correct_hashes = [ + # ((secret,user),hash) + + # + # generated using postgres 8.1 + # + (('mypass', 'postgres'), 'md55fba2ea04fd36069d2574ea71c8efe9d'), + (('mypass', 'root'), 'md540c31989b20437833f697e485811254b'), + (("testpassword",'testuser'), 'md5d4fc5129cc2c25465a5370113ae9835f'), + + # + # custom + # + + # verify unicode->utf8 + ((UPASS_TABLE, 'postgres'), 'md5cb9f11283265811ce076db86d18a22d2'), + ] + known_unidentified_hashes = [ + # bad 'z' char in otherwise correct hash + 'md54zc31989b20437833f697e485811254b', + ] + +#============================================================================= +# (netbsd's) sha1 crypt +#============================================================================= +class _sha1_crypt_test(HandlerCase): + handler = hash.sha1_crypt + + known_correct_hashes = [ + # + # custom + # + ("password", "$sha1$19703$iVdJqfSE$v4qYKl1zqYThwpjJAoKX6UvlHq/a"), + ("password", "$sha1$21773$uV7PTeux$I9oHnvwPZHMO0Nq6/WgyGV/tDJIH"), + (UPASS_TABLE, '$sha1$40000$uJ3Sp7LE$.VEmLO5xntyRFYihC7ggd3297T/D'), + ] + + known_malformed_hashes = [ + # bad char in otherwise correct hash + '$sha1$21773$u!7PTeux$I9oHnvwPZHMO0Nq6/WgyGV/tDJIH', + + # zero padded rounds + '$sha1$01773$uV7PTeux$I9oHnvwPZHMO0Nq6/WgyGV/tDJIH', + + # too many fields + '$sha1$21773$uV7PTeux$I9oHnvwPZHMO0Nq6/WgyGV/tDJIH$', + + # empty rounds field + '$sha1$$uV7PTeux$I9oHnvwPZHMO0Nq6/WgyGV/tDJIH$', + ] + + platform_crypt_support = [ + ("netbsd", True), + ("freebsd|openbsd|solaris|darwin", False), + ("linux", None), # may be present if libxcrypt is in use + ] + +# create test cases for specific backends +sha1_crypt_os_crypt_test = _sha1_crypt_test.create_backend_case("os_crypt") +sha1_crypt_builtin_test = _sha1_crypt_test.create_backend_case("builtin") + +#============================================================================= +# roundup +#============================================================================= + +# NOTE: all roundup hashes use PrefixWrapper, +# so there's nothing natively to test. +# so we just have a few quick cases... + +class RoundupTest(TestCase): + + def _test_pair(self, h, secret, hash): + self.assertTrue(h.verify(secret, hash)) + self.assertFalse(h.verify('x'+secret, hash)) + + def test_pairs(self): + self._test_pair( + hash.ldap_hex_sha1, + "sekrit", + '{SHA}8d42e738c7adee551324955458b5e2c0b49ee655') + + self._test_pair( + hash.ldap_hex_md5, + "sekrit", + '{MD5}ccbc53f4464604e714f69dd11138d8b5') + + self._test_pair( + hash.ldap_des_crypt, + "sekrit", + '{CRYPT}nFia0rj2TT59A') + + self._test_pair( + hash.roundup_plaintext, + "sekrit", + '{plaintext}sekrit') + + self._test_pair( + hash.ldap_pbkdf2_sha1, + "sekrit", + '{PBKDF2}5000$7BvbBq.EZzz/O0HuwX3iP.nAG3s$g3oPnFFaga2BJaX5PoPRljl4XIE') + +#============================================================================= +# sha256-crypt +#============================================================================= +class _sha256_crypt_test(HandlerCase): + handler = hash.sha256_crypt + + known_correct_hashes = [ + # + # from JTR 1.7.9 + # + ('U*U*U*U*', '$5$LKO/Ute40T3FNF95$U0prpBQd4PloSGU0pnpM4z9wKn4vZ1.jsrzQfPqxph9'), + ('U*U***U', '$5$LKO/Ute40T3FNF95$fdgfoJEBoMajNxCv3Ru9LyQ0xZgv0OBMQoq80LQ/Qd.'), + ('U*U***U*', '$5$LKO/Ute40T3FNF95$8Ry82xGnnPI/6HtFYnvPBTYgOL23sdMXn8C29aO.x/A'), + ('*U*U*U*U', '$5$9mx1HkCz7G1xho50$O7V7YgleJKLUhcfk9pgzdh3RapEaWqMtEp9UUBAKIPA'), + ('', '$5$kc7lRD1fpYg0g.IP$d7CMTcEqJyTXyeq8hTdu/jB/I6DGkoo62NXbHIR7S43'), + + # + # custom tests + # + ('', '$5$rounds=10428$uy/jIAhCetNCTtb0$YWvUOXbkqlqhyoPMpN8BMe.ZGsGx2aBvxTvDFI613c3'), + (' ', '$5$rounds=10376$I5lNtXtRmf.OoMd8$Ko3AI1VvTANdyKhBPavaRjJzNpSatKU6QVN9uwS9MH.'), + ('test', '$5$rounds=11858$WH1ABM5sKhxbkgCK$aTQsjPkz0rBsH3lQlJxw9HDTDXPKBxC0LlVeV69P.t1'), + ('Compl3X AlphaNu3meric', '$5$rounds=10350$o.pwkySLCzwTdmQX$nCMVsnF3TXWcBPOympBUUSQi6LGGloZoOsVJMGJ09UB'), + ('4lpHa N|_|M3r1K W/ Cur5Es: #$%(*)(*%#', '$5$rounds=11944$9dhlu07dQMRWvTId$LyUI5VWkGFwASlzntk1RLurxX54LUhgAcJZIt0pYGT7'), + (u('with unic\u00D6de'), '$5$rounds=1000$IbG0EuGQXw5EkMdP$LQ5AfPf13KufFsKtmazqnzSGZ4pxtUNw3woQ.ELRDF4'), + ] + + if TEST_MODE("full"): + # builtin alg was changed in 1.6, and had possibility of fencepost + # errors near rounds that are multiples of 42. these hashes test rounds + # 1004..1012 (42*24=1008 +/- 4) to ensure no mistakes were made. + # (also relying on fuzz testing against os_crypt backend). + known_correct_hashes.extend([ + ("secret", '$5$rounds=1004$nacl$oiWPbm.kQ7.jTCZoOtdv7/tO5mWv/vxw5yTqlBagVR7'), + ("secret", '$5$rounds=1005$nacl$6Mo/TmGDrXxg.bMK9isRzyWH3a..6HnSVVsJMEX7ud/'), + ("secret", '$5$rounds=1006$nacl$I46VwuAiUBwmVkfPFakCtjVxYYaOJscsuIeuZLbfKID'), + ("secret", '$5$rounds=1007$nacl$9fY4j1AV3N/dV/YMUn1enRHKH.7nEL4xf1wWB6wfDD4'), + ("secret", '$5$rounds=1008$nacl$CiFWCfn8ODmWs0I1xAdXFo09tM8jr075CyP64bu3by9'), + ("secret", '$5$rounds=1009$nacl$QtpFX.CJHgVQ9oAjVYStxAeiU38OmFILWm684c6FyED'), + ("secret", '$5$rounds=1010$nacl$ktAwXuT5WbjBW/0ZU1eNMpqIWY1Sm4twfRE1zbZyo.B'), + ("secret", '$5$rounds=1011$nacl$QJWLBEhO9qQHyMx4IJojSN9sS41P1Yuz9REddxdO721'), + ("secret", '$5$rounds=1012$nacl$mmf/k2PkbBF4VCtERgky3bEVavmLZKFwAcvxD1p3kV2'), + ]) + + known_malformed_hashes = [ + # bad char in otherwise correct hash + '$5$rounds=10428$uy/:jIAhCetNCTtb0$YWvUOXbkqlqhyoPMpN8BMeZGsGx2aBvxTvDFI613c3', + + # zero-padded rounds + '$5$rounds=010428$uy/jIAhCetNCTtb0$YWvUOXbkqlqhyoPMpN8BMe.ZGsGx2aBvxTvDFI613c3', + + # extra "$" + '$5$rounds=10428$uy/jIAhCetNCTtb0$YWvUOXbkqlqhyoPMpN8BMe.ZGsGx2aBvxTvDFI613c3$', + ] + + known_correct_configs = [ + # config, secret, result + + # + # taken from official specification at http://www.akkadia.org/drepper/SHA-crypt.txt + # + ( "$5$saltstring", "Hello world!", + "$5$saltstring$5B8vYYiY.CVt1RlTTf8KbXBH3hsxY/GNooZaBBGWEc5" ), + ( "$5$rounds=10000$saltstringsaltstring", "Hello world!", + "$5$rounds=10000$saltstringsaltst$3xv.VbSHBb41AL9AvLeujZkZRBAwqFMz2." + "opqey6IcA" ), + ( "$5$rounds=5000$toolongsaltstring", "This is just a test", + "$5$rounds=5000$toolongsaltstrin$Un/5jzAHMgOGZ5.mWJpuVolil07guHPvOW8" + "mGRcvxa5" ), + ( "$5$rounds=1400$anotherlongsaltstring", + "a very much longer text to encrypt. This one even stretches over more" + "than one line.", + "$5$rounds=1400$anotherlongsalts$Rx.j8H.h8HjEDGomFU8bDkXm3XIUnzyxf12" + "oP84Bnq1" ), + ( "$5$rounds=77777$short", + "we have a short salt string but not a short password", + "$5$rounds=77777$short$JiO1O3ZpDAxGJeaDIuqCoEFysAe1mZNJRs3pw0KQRd/" ), + ( "$5$rounds=123456$asaltof16chars..", "a short string", + "$5$rounds=123456$asaltof16chars..$gP3VQ/6X7UUEW3HkBn2w1/Ptq2jxPyzV/" + "cZKmF/wJvD" ), + ( "$5$rounds=10$roundstoolow", "the minimum number is still observed", + "$5$rounds=1000$roundstoolow$yfvwcWrQ8l/K0DAWyuPMDNHpIVlTQebY9l/gL97" + "2bIC" ), + ] + + filter_config_warnings = True # rounds too low, salt too small + + platform_crypt_support = [ + ("freebsd(9|1\d)|linux", True), + ("freebsd8", None), # added in freebsd 8.3 + ("freebsd|openbsd|netbsd|darwin", False), + ("solaris", None), # depends on policy + ] + +# create test cases for specific backends +sha256_crypt_os_crypt_test = _sha256_crypt_test.create_backend_case("os_crypt") +sha256_crypt_builtin_test = _sha256_crypt_test.create_backend_case("builtin") + +#============================================================================= +# test sha512-crypt +#============================================================================= +class _sha512_crypt_test(HandlerCase): + handler = hash.sha512_crypt + + known_correct_hashes = [ + # + # from JTR 1.7.9 + # + ('U*U*U*U*', "$6$LKO/Ute40T3FNF95$6S/6T2YuOIHY0N3XpLKABJ3soYcXD9mB7uVbtEZDj/LNscVhZoZ9DEH.sBciDrMsHOWOoASbNLTypH/5X26gN0"), + ('U*U***U', "$6$LKO/Ute40T3FNF95$wK80cNqkiAUzFuVGxW6eFe8J.fSVI65MD5yEm8EjYMaJuDrhwe5XXpHDJpwF/kY.afsUs1LlgQAaOapVNbggZ1"), + ('U*U***U*', "$6$LKO/Ute40T3FNF95$YS81pp1uhOHTgKLhSMtQCr2cDiUiN03Ud3gyD4ameviK1Zqz.w3oXsMgO6LrqmIEcG3hiqaUqHi/WEE2zrZqa/"), + ('*U*U*U*U', "$6$OmBOuxFYBZCYAadG$WCckkSZok9xhp4U1shIZEV7CCVwQUwMVea7L3A77th6SaE9jOPupEMJB.z0vIWCDiN9WLh2m9Oszrj5G.gt330"), + ('', "$6$ojWH1AiTee9x1peC$QVEnTvRVlPRhcLQCk/HnHaZmlGAAjCfrAN0FtOsOnUk5K5Bn/9eLHHiRzrTzaIKjW9NTLNIBUCtNVOowWS2mN."), + + # + # custom tests + # + ('', '$6$rounds=11021$KsvQipYPWpr93wWP$v7xjI4X6vyVptJjB1Y02vZC5SaSijBkGmq1uJhPr3cvqvvkd42Xvo48yLVPFt8dvhCsnlUgpX.//Cxn91H4qy1'), + (' ', '$6$rounds=11104$ED9SA4qGmd57Fq2m$q/.PqACDM/JpAHKmr86nkPzzuR5.YpYa8ZJJvI8Zd89ZPUYTJExsFEIuTYbM7gAGcQtTkCEhBKmp1S1QZwaXx0'), + ('test', '$6$rounds=11531$G/gkPn17kHYo0gTF$Kq.uZBHlSBXyzsOJXtxJruOOH4yc0Is13uY7yK0PvAvXxbvc1w8DO1RzREMhKsc82K/Jh8OquV8FZUlreYPJk1'), + ('Compl3X AlphaNu3meric', '$6$rounds=10787$wakX8nGKEzgJ4Scy$X78uqaX1wYXcSCtS4BVYw2trWkvpa8p7lkAtS9O/6045fK4UB2/Jia0Uy/KzCpODlfVxVNZzCCoV9s2hoLfDs/'), + ('4lpHa N|_|M3r1K W/ Cur5Es: #$%(*)(*%#', '$6$rounds=11065$5KXQoE1bztkY5IZr$Jf6krQSUKKOlKca4hSW07MSerFFzVIZt/N3rOTsUgKqp7cUdHrwV8MoIVNCk9q9WL3ZRMsdbwNXpVk0gVxKtz1'), + + # ensures utf-8 used for unicode + (UPASS_TABLE, '$6$rounds=40000$PEZTJDiyzV28M3.m$GTlnzfzGB44DGd1XqlmC4erAJKCP.rhvLvrYxiT38htrNzVGBnplFOHjejUGVrCfusGWxLQCc3pFO0A/1jYYr0'), + ] + + known_malformed_hashes = [ + # zero-padded rounds + '$6$rounds=011021$KsvQipYPWpr93wWP$v7xjI4X6vyVptJjB1Y02vZC5SaSijBkGmq1uJhPr3cvqvvkd42Xvo48yLVPFt8dvhCsnlUgpX.//Cxn91H4qy1', + # bad char in otherwise correct hash + '$6$rounds=11021$KsvQipYPWpr9:wWP$v7xjI4X6vyVptJjB1Y02vZC5SaSijBkGmq1uJhPr3cvqvvkd42Xvo48yLVPFt8dvhCsnlUgpX.//Cxn91H4qy1', + ] + + known_correct_configs = [ + # config, secret, result + + # + # taken from official specification at http://www.akkadia.org/drepper/SHA-crypt.txt + # + ("$6$saltstring", "Hello world!", + "$6$saltstring$svn8UoSVapNtMuq1ukKS4tPQd8iKwSMHWjl/O817G3uBnIFNjnQJu" + "esI68u4OTLiBFdcbYEdFCoEOfaS35inz1" ), + + ( "$6$rounds=10000$saltstringsaltstring", "Hello world!", + "$6$rounds=10000$saltstringsaltst$OW1/O6BYHV6BcXZu8QVeXbDWra3Oeqh0sb" + "HbbMCVNSnCM/UrjmM0Dp8vOuZeHBy/YTBmSK6H9qs/y3RnOaw5v." ), + + ( "$6$rounds=5000$toolongsaltstring", "This is just a test", + "$6$rounds=5000$toolongsaltstrin$lQ8jolhgVRVhY4b5pZKaysCLi0QBxGoNeKQ" + "zQ3glMhwllF7oGDZxUhx1yxdYcz/e1JSbq3y6JMxxl8audkUEm0" ), + + ( "$6$rounds=1400$anotherlongsaltstring", + "a very much longer text to encrypt. This one even stretches over more" + "than one line.", + "$6$rounds=1400$anotherlongsalts$POfYwTEok97VWcjxIiSOjiykti.o/pQs.wP" + "vMxQ6Fm7I6IoYN3CmLs66x9t0oSwbtEW7o7UmJEiDwGqd8p4ur1" ), + + ( "$6$rounds=77777$short", + "we have a short salt string but not a short password", + "$6$rounds=77777$short$WuQyW2YR.hBNpjjRhpYD/ifIw05xdfeEyQoMxIXbkvr0g" + "ge1a1x3yRULJ5CCaUeOxFmtlcGZelFl5CxtgfiAc0" ), + + ( "$6$rounds=123456$asaltof16chars..", "a short string", + "$6$rounds=123456$asaltof16chars..$BtCwjqMJGx5hrJhZywWvt0RLE8uZ4oPwc" + "elCjmw2kSYu.Ec6ycULevoBK25fs2xXgMNrCzIMVcgEJAstJeonj1" ), + + ( "$6$rounds=10$roundstoolow", "the minimum number is still observed", + "$6$rounds=1000$roundstoolow$kUMsbe306n21p9R.FRkW3IGn.S9NPN0x50YhH1x" + "hLsPuWGsUSklZt58jaTfF4ZEQpyUNGc0dqbpBYYBaHHrsX." ), + ] + + filter_config_warnings = True # rounds too low, salt too small + + platform_crypt_support = _sha256_crypt_test.platform_crypt_support + +# create test cases for specific backends +sha512_crypt_os_crypt_test = _sha512_crypt_test.create_backend_case("os_crypt") +sha512_crypt_builtin_test = _sha512_crypt_test.create_backend_case("builtin") + +#============================================================================= +# sun md5 crypt +#============================================================================= +class sun_md5_crypt_test(HandlerCase): + handler = hash.sun_md5_crypt + + # TODO: this scheme needs some real test vectors, especially due to + # the "bare salt" issue which plagued the official parser. + known_correct_hashes = [ + # + # http://forums.halcyoninc.com/showthread.php?t=258 + # + ("Gpcs3_adm", "$md5$zrdhpMlZ$$wBvMOEqbSjU.hu5T2VEP01"), + + # + # http://www.c0t0d0s0.org/archives/4453-Less-known-Solaris-features-On-passwords-Part-2-Using-stronger-password-hashing.html + # + ("aa12345678", "$md5$vyy8.OVF$$FY4TWzuauRl4.VQNobqMY."), + + # + # http://www.cuddletech.com/blog/pivot/entry.php?id=778 + # + ("this", "$md5$3UqYqndY$$6P.aaWOoucxxq.l00SS9k0"), + + # + # http://compgroups.net/comp.unix.solaris/password-file-in-linux-and-solaris-8-9 + # + ("passwd", "$md5$RPgLF6IJ$WTvAlUJ7MqH5xak2FMEwS/"), + + # + # source: http://solaris-training.com/301_HTML/docs/deepdiv.pdf page 27 + # FIXME: password unknown + # "$md5,rounds=8000$kS9FT1JC$$mnUrRO618lLah5iazwJ9m1" + + # + # source: http://www.visualexams.com/310-303.htm + # XXX: this has 9 salt chars unlike all other hashes. is that valid? + # FIXME: password unknown + # "$md5,rounds=2006$2amXesSj5$$kCF48vfPsHDjlKNXeEw7V." + # + + # + # custom + # + + # ensures utf-8 used for unicode + (UPASS_TABLE, '$md5,rounds=5000$10VYDzAA$$1arAVtMA3trgE1qJ2V0Ez1'), + ] + + known_correct_configs = [ + # (config, secret, hash) + + #--------------------------- + # test salt string handling + # + # these tests attempt to verify that passlib is handling + # the "bare salt" issue (see sun md5 crypt docs) + # in a sane manner + #--------------------------- + + # config with "$" suffix, hash strings with "$$" suffix, + # should all be treated the same, with one "$" added to salt digest. + ("$md5$3UqYqndY$", + "this", "$md5$3UqYqndY$$6P.aaWOoucxxq.l00SS9k0"), + ("$md5$3UqYqndY$$.................DUMMY", + "this", "$md5$3UqYqndY$$6P.aaWOoucxxq.l00SS9k0"), + + # config with no suffix, hash strings with "$" suffix, + # should all be treated the same, and no suffix added to salt digest. + # NOTE: this is just a guess re: config w/ no suffix, + # but otherwise there's no sane way to encode bare_salt=False + # within config string. + ("$md5$3UqYqndY", + "this", "$md5$3UqYqndY$HIZVnfJNGCPbDZ9nIRSgP1"), + ("$md5$3UqYqndY$.................DUMMY", + "this", "$md5$3UqYqndY$HIZVnfJNGCPbDZ9nIRSgP1"), + ] + + known_malformed_hashes = [ + # unexpected end of hash + "$md5,rounds=5000", + + # bad rounds + "$md5,rounds=500A$xxxx", + "$md5,rounds=0500$xxxx", + "$md5,rounds=0$xxxx", + + # bad char in otherwise correct hash + "$md5$RPgL!6IJ$WTvAlUJ7MqH5xak2FMEwS/", + + # digest too short + "$md5$RPgLa6IJ$WTvAlUJ7MqH5xak2FMEwS", + + # digest too long + "$md5$RPgLa6IJ$WTvAlUJ7MqH5xak2FMEwS/.", + + # 2+ "$" at end of salt in config + # NOTE: not sure what correct behavior is, so forbidding format for now. + "$md5$3UqYqndY$$", + + # 3+ "$" at end of salt in hash + # NOTE: not sure what correct behavior is, so forbidding format for now. + "$md5$RPgLa6IJ$$$WTvAlUJ7MqH5xak2FMEwS/", + + ] + + platform_crypt_support = [ + ("solaris", True), + ("freebsd|openbsd|netbsd|linux|darwin", False), + ] + def do_verify(self, secret, hash): + # Override to fake error for "$..." hash string listed in known_correct_configs (above) + # These have to be hash strings, in order to test bare salt issue. + if isinstance(hash, str) and hash.endswith("$.................DUMMY"): + raise ValueError("pretending '$...' stub hash is config string") + return self.handler.verify(secret, hash) + +#============================================================================= +# unix disabled / fallback +#============================================================================= +class unix_disabled_test(HandlerCase): + handler = hash.unix_disabled +# accepts_all_hashes = True # TODO: turn this off. + + known_correct_hashes = [ + # everything should hash to "!" (or "*" on BSD), + # and nothing should verify against either string + ("password", "!"), + (UPASS_TABLE, "*"), + ] + + known_unidentified_hashes = [ + # should never identify anything crypt() could return... + "$1$xxx", + "abc", + "./az", + "{SHA}xxx", + ] + + def test_76_hash_border(self): + # so empty strings pass + self.accepts_all_hashes = True + super(unix_disabled_test, self).test_76_hash_border() + + def test_90_special(self): + """test marker option & special behavior""" + warnings.filterwarnings("ignore", "passing settings to .*.hash\(\) is deprecated") + handler = self.handler + + # preserve hash if provided + self.assertEqual(handler.genhash("stub", "!asd"), "!asd") + + # use marker if no hash + self.assertEqual(handler.genhash("stub", ""), handler.default_marker) + self.assertEqual(handler.hash("stub"), handler.default_marker) + self.assertEqual(handler.using().default_marker, handler.default_marker) + + # custom marker + self.assertEqual(handler.genhash("stub", "", marker="*xxx"), "*xxx") + self.assertEqual(handler.hash("stub", marker="*xxx"), "*xxx") + self.assertEqual(handler.using(marker="*xxx").hash("stub"), "*xxx") + + # reject invalid marker + self.assertRaises(ValueError, handler.genhash, 'stub', "", marker='abc') + self.assertRaises(ValueError, handler.hash, 'stub', marker='abc') + self.assertRaises(ValueError, handler.using, marker='abc') + +class unix_fallback_test(HandlerCase): + handler = hash.unix_fallback + accepts_all_hashes = True + + known_correct_hashes = [ + # *everything* should hash to "!", and nothing should verify + ("password", "!"), + (UPASS_TABLE, "!"), + ] + + # silence annoying deprecation warning + def setUp(self): + super(unix_fallback_test, self).setUp() + warnings.filterwarnings("ignore", "'unix_fallback' is deprecated") + + def test_90_wildcard(self): + """test enable_wildcard flag""" + h = self.handler + self.assertTrue(h.verify('password','', enable_wildcard=True)) + self.assertFalse(h.verify('password','')) + for c in "!*x": + self.assertFalse(h.verify('password',c, enable_wildcard=True)) + self.assertFalse(h.verify('password',c)) + + def test_91_preserves_existing(self): + """test preserves existing disabled hash""" + handler = self.handler + + # use marker if no hash + self.assertEqual(handler.genhash("stub", ""), "!") + self.assertEqual(handler.hash("stub"), "!") + + # use hash if provided and valid + self.assertEqual(handler.genhash("stub", "!asd"), "!asd") + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/test_handlers_argon2.py b/ansible/lib/python3.11/site-packages/passlib/tests/test_handlers_argon2.py new file mode 100644 index 000000000..e77176934 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/tests/test_handlers_argon2.py @@ -0,0 +1,507 @@ +"""passlib.tests.test_handlers_argon2 - tests for passlib hash algorithms""" +#============================================================================= +# imports +#============================================================================= +# core +import logging +log = logging.getLogger(__name__) +import re +import warnings +# site +# pkg +from passlib import hash +from passlib.utils.compat import unicode +from passlib.tests.utils import HandlerCase, TEST_MODE +from passlib.tests.test_handlers import UPASS_TABLE, PASS_TABLE_UTF8 +# module + +#============================================================================= +# a bunch of tests lifted nearlky verbatim from official argon2 UTs... +# https://github.com/P-H-C/phc-winner-argon2/blob/master/src/test.c +#============================================================================= +def hashtest(version, t, logM, p, secret, salt, hex_digest, hash): + return dict(version=version, rounds=t, logM=logM, memory_cost=1< max uint32 + "$argon2i$v=19$m=65536,t=8589934592,p=4$c29tZXNhbHQAAAAAAAAAAA$QWLzI4TY9HkL2ZTLc8g6SinwdhZewYrzz9zxCo0bkGY", + + # unexpected param + "$argon2i$v=19$m=65536,t=2,p=4,q=5$c29tZXNhbHQAAAAAAAAAAA$QWLzI4TY9HkL2ZTLc8g6SinwdhZewYrzz9zxCo0bkGY", + + # wrong param order + "$argon2i$v=19$t=2,m=65536,p=4,q=5$c29tZXNhbHQAAAAAAAAAAA$QWLzI4TY9HkL2ZTLc8g6SinwdhZewYrzz9zxCo0bkGY", + + # constraint violation: m < 8 * p + "$argon2i$v=19$m=127,t=2,p=16$c29tZXNhbHQ$IMit9qkFULCMA/ViizL57cnTLOa5DiVM9eMwpAvPwr4", + ] + + known_parsehash_results = [ + ('$argon2i$v=19$m=256,t=2,p=3$c29tZXNhbHQ$AJFIsNZTMKTAewB4+ETN1A', + dict(type="i", memory_cost=256, rounds=2, parallelism=3, salt=b'somesalt', + checksum=b'\x00\x91H\xb0\xd6S0\xa4\xc0{\x00x\xf8D\xcd\xd4')), + ] + + def setUpWarnings(self): + super(_base_argon2_test, self).setUpWarnings() + warnings.filterwarnings("ignore", ".*Using argon2pure backend.*") + + def do_stub_encrypt(self, handler=None, **settings): + if self.backend == "argon2_cffi": + # overriding default since no way to get stub config from argon2._calc_hash() + # (otherwise test_21b_max_rounds blocks trying to do max rounds) + handler = (handler or self.handler).using(**settings) + self = handler(use_defaults=True) + self.checksum = self._stub_checksum + assert self.checksum + return self.to_string() + else: + return super(_base_argon2_test, self).do_stub_encrypt(handler, **settings) + + def test_03_legacy_hash_workflow(self): + # override base method + raise self.skipTest("legacy 1.6 workflow not supported") + + def test_keyid_parameter(self): + # NOTE: keyid parameter currently not supported by official argon2 hash parser, + # even though it's mentioned in the format spec. + # we're trying to be consistent w/ this, so hashes w/ keyid should + # always through a NotImplementedError. + self.assertRaises(NotImplementedError, self.handler.verify, 'password', + "$argon2i$v=19$m=65536,t=2,p=4,keyid=ABCD$c29tZXNhbHQ$" + "IMit9qkFULCMA/ViizL57cnTLOa5DiVM9eMwpAvPwr4") + + def test_data_parameter(self): + # NOTE: argon2 c library doesn't support passing in a data parameter to argon2_hash(); + # but argon2_verify() appears to parse that info... but then discards it (!?). + # not sure what proper behavior is, filed issue -- https://github.com/P-H-C/phc-winner-argon2/issues/143 + # For now, replicating behavior we have for the two backends, to detect when things change. + handler = self.handler + + # ref hash of 'password' when 'data' is correctly passed into argon2() + sample1 = '$argon2i$v=19$m=512,t=2,p=2,data=c29tZWRhdGE$c29tZXNhbHQ$KgHyCesFyyjkVkihZ5VNFw' + + # ref hash of 'password' when 'data' is silently discarded (same digest as w/o data) + sample2 = '$argon2i$v=19$m=512,t=2,p=2,data=c29tZWRhdGE$c29tZXNhbHQ$uEeXt1dxN1iFKGhklseW4w' + + # hash of 'password' w/o the data field + sample3 = '$argon2i$v=19$m=512,t=2,p=2$c29tZXNhbHQ$uEeXt1dxN1iFKGhklseW4w' + + # + # test sample 1 + # + + if self.backend == "argon2_cffi": + # argon2_cffi v16.1 would incorrectly return False here. + # but v16.2 patches so it throws error on data parameter. + # our code should detect that, and adapt it into a NotImplementedError + self.assertRaises(NotImplementedError, handler.verify, "password", sample1) + + # incorrectly returns sample3, dropping data parameter + self.assertEqual(handler.genhash("password", sample1), sample3) + + else: + assert self.backend == "argon2pure" + # should parse and verify + self.assertTrue(handler.verify("password", sample1)) + + # should preserve sample1 + self.assertEqual(handler.genhash("password", sample1), sample1) + + # + # test sample 2 + # + + if self.backend == "argon2_cffi": + # argon2_cffi v16.1 would incorrectly return True here. + # but v16.2 patches so it throws error on data parameter. + # our code should detect that, and adapt it into a NotImplementedError + self.assertRaises(NotImplementedError, handler.verify,"password", sample2) + + # incorrectly returns sample3, dropping data parameter + self.assertEqual(handler.genhash("password", sample1), sample3) + + else: + assert self.backend == "argon2pure" + # should parse, but fail to verify + self.assertFalse(self.handler.verify("password", sample2)) + + # should return sample1 (corrected digest) + self.assertEqual(handler.genhash("password", sample2), sample1) + + def test_keyid_and_data_parameters(self): + # test combination of the two, just in case + self.assertRaises(NotImplementedError, self.handler.verify, 'stub', + "$argon2i$v=19$m=65536,t=2,p=4,keyid=ABCD,data=EFGH$c29tZXNhbHQ$" + "IMit9qkFULCMA/ViizL57cnTLOa5DiVM9eMwpAvPwr4") + + def test_type_kwd(self): + cls = self.handler + + # XXX: this mirrors test_30_HasManyIdents(); + # maybe switch argon2 class to use that mixin instead of "type" kwd? + + # check settings + self.assertTrue("type" in cls.setting_kwds) + + # check supported type_values + for value in cls.type_values: + self.assertIsInstance(value, unicode) + self.assertTrue("i" in cls.type_values) + self.assertTrue("d" in cls.type_values) + + # check default + self.assertTrue(cls.type in cls.type_values) + + # check constructor validates ident correctly. + handler = cls + hash = self.get_sample_hash()[1] + kwds = handler.parsehash(hash) + del kwds['type'] + + # ... accepts good type + handler(type=cls.type, **kwds) + + # XXX: this is policy "ident" uses, maybe switch to it? + # # ... requires type w/o defaults + # self.assertRaises(TypeError, handler, **kwds) + handler(**kwds) + + # ... supplies default type + handler(use_defaults=True, **kwds) + + # ... rejects bad type + self.assertRaises(ValueError, handler, type='xXx', **kwds) + + def test_type_using(self): + handler = self.handler + + # XXX: this mirrors test_has_many_idents_using(); + # maybe switch argon2 class to use that mixin instead of "type" kwd? + + orig_type = handler.type + for alt_type in handler.type_values: + if alt_type != orig_type: + break + else: + raise AssertionError("expected to find alternate type: default=%r values=%r" % + (orig_type, handler.type_values)) + + def effective_type(cls): + return cls(use_defaults=True).type + + # keep default if nothing else specified + subcls = handler.using() + self.assertEqual(subcls.type, orig_type) + + # accepts alt type + subcls = handler.using(type=alt_type) + self.assertEqual(subcls.type, alt_type) + self.assertEqual(handler.type, orig_type) + + # check subcls actually *generates* default type, + # and that we didn't affect orig handler + self.assertEqual(effective_type(subcls), alt_type) + self.assertEqual(effective_type(handler), orig_type) + + # rejects bad type + self.assertRaises(ValueError, handler.using, type='xXx') + + # honor 'type' alias + subcls = handler.using(type=alt_type) + self.assertEqual(subcls.type, alt_type) + self.assertEqual(handler.type, orig_type) + + # check type aliases are being honored + self.assertEqual(effective_type(handler.using(type="I")), "i") + + def test_needs_update_w_type(self): + handler = self.handler + + hash = handler.hash("stub") + self.assertFalse(handler.needs_update(hash)) + + hash2 = re.sub(r"\$argon2\w+\$", "$argon2d$", hash) + self.assertTrue(handler.needs_update(hash2)) + + def test_needs_update_w_version(self): + handler = self.handler.using(memory_cost=65536, time_cost=2, parallelism=4, + digest_size=32) + hash = ("$argon2i$m=65536,t=2,p=4$c29tZXNhbHQAAAAAAAAAAA$" + "QWLzI4TY9HkL2ZTLc8g6SinwdhZewYrzz9zxCo0bkGY") + if handler.max_version == 0x10: + self.assertFalse(handler.needs_update(hash)) + else: + self.assertTrue(handler.needs_update(hash)) + + def test_argon_byte_encoding(self): + """verify we're using right base64 encoding for argon2""" + handler = self.handler + if handler.version != 0x13: + # TODO: make this fatal, and add refs for other version. + raise self.skipTest("handler uses wrong version for sample hashes") + + # 8 byte salt + salt = b'somesalt' + temp = handler.using(memory_cost=256, time_cost=2, parallelism=2, salt=salt, + checksum_size=32, type="i") + hash = temp.hash("password") + self.assertEqual(hash, "$argon2i$v=19$m=256,t=2,p=2" + "$c29tZXNhbHQ" + "$T/XOJ2mh1/TIpJHfCdQan76Q5esCFVoT5MAeIM1Oq2E") + + # 16 byte salt + salt = b'somesalt\x00\x00\x00\x00\x00\x00\x00\x00' + temp = handler.using(memory_cost=256, time_cost=2, parallelism=2, salt=salt, + checksum_size=32, type="i") + hash = temp.hash("password") + self.assertEqual(hash, "$argon2i$v=19$m=256,t=2,p=2" + "$c29tZXNhbHQAAAAAAAAAAA" + "$rqnbEp1/jFDUEKZZmw+z14amDsFqMDC53dIe57ZHD38") + + class FuzzHashGenerator(HandlerCase.FuzzHashGenerator): + + settings_map = HandlerCase.FuzzHashGenerator.settings_map.copy() + settings_map.update(memory_cost="random_memory_cost", type="random_type") + + def random_type(self): + return self.rng.choice(self.handler.type_values) + + def random_memory_cost(self): + if self.test.backend == "argon2pure": + return self.randintgauss(128, 384, 256, 128) + else: + return self.randintgauss(128, 32767, 16384, 4096) + + # TODO: fuzz parallelism, digest_size + +#----------------------------------------- +# test suites for specific backends +#----------------------------------------- + +class argon2_argon2_cffi_test(_base_argon2_test.create_backend_case("argon2_cffi")): + + # add some more test vectors that take too long under argon2pure + known_correct_hashes = _base_argon2_test.known_correct_hashes + [ + # + # sample hashes from argon2 cffi package's unittests, + # which in turn were generated by official argon2 cmdline tool. + # + + # v1.2, type I, w/o a version tag + ('password', "$argon2i$m=65536,t=2,p=4$c29tZXNhbHQAAAAAAAAAAA$" + "QWLzI4TY9HkL2ZTLc8g6SinwdhZewYrzz9zxCo0bkGY"), + + # v1.3, type I + ('password', "$argon2i$v=19$m=65536,t=2,p=4$c29tZXNhbHQ$" + "IMit9qkFULCMA/ViizL57cnTLOa5DiVM9eMwpAvPwr4"), + + # v1.3, type D + ('password', "$argon2d$v=19$m=65536,t=2,p=4$c29tZXNhbHQ$" + "cZn5d+rFh+ZfuRhm2iGUGgcrW5YLeM6q7L3vBsdmFA0"), + + # v1.3, type ID + ('password', "$argon2id$v=19$m=65536,t=2,p=4$c29tZXNhbHQ$" + "GpZ3sK/oH9p7VIiV56G/64Zo/8GaUw434IimaPqxwCo"), + + # + # custom + # + + # ensure trailing null bytes handled correctly + ('password\x00', "$argon2i$v=19$m=65536,t=2,p=4$c29tZXNhbHQ$" + "Vpzuc0v0SrP88LcVvmg+z5RoOYpMDKH/lt6O+CZabIQ"), + + ] + + # add reference hashes from argon2 clib tests + known_correct_hashes.extend( + (info['secret'], info['hash']) for info in reference_data + if info['logM'] <= (18 if TEST_MODE("full") else 16) + ) + +class argon2_argon2pure_test(_base_argon2_test.create_backend_case("argon2pure")): + + # XXX: setting max_threads at 1 to prevent argon2pure from using multiprocessing, + # which causes big problems when testing under pypy. + # would like a "pure_use_threads" option instead, to make it use multiprocessing.dummy instead. + handler = hash.argon2.using(memory_cost=32, parallelism=2) + + # don't use multiprocessing for unittests, makes it a lot harder to ctrl-c + # XXX: make this controlled by env var? + handler.pure_use_threads = True + + # add reference hashes from argon2 clib tests + known_correct_hashes = _base_argon2_test.known_correct_hashes[:] + + known_correct_hashes.extend( + (info['secret'], info['hash']) for info in reference_data + if info['logM'] < 16 + ) + + class FuzzHashGenerator(_base_argon2_test.FuzzHashGenerator): + + def random_rounds(self): + # decrease default rounds for fuzz testing to speed up volume. + return self.randintgauss(1, 3, 2, 1) + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/test_handlers_bcrypt.py b/ansible/lib/python3.11/site-packages/passlib/tests/test_handlers_bcrypt.py new file mode 100644 index 000000000..64fc8bff9 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/tests/test_handlers_bcrypt.py @@ -0,0 +1,688 @@ +"""passlib.tests.test_handlers - tests for passlib hash algorithms""" +#============================================================================= +# imports +#============================================================================= +from __future__ import with_statement +# core +import logging; log = logging.getLogger(__name__) +import os +import warnings +# site +# pkg +from passlib import hash +from passlib.handlers.bcrypt import IDENT_2, IDENT_2X +from passlib.utils import repeat_string, to_bytes, is_safe_crypt_input +from passlib.utils.compat import irange, PY3 +from passlib.tests.utils import HandlerCase, TEST_MODE +from passlib.tests.test_handlers import UPASS_TABLE +# module + +#============================================================================= +# bcrypt +#============================================================================= +class _bcrypt_test(HandlerCase): + """base for BCrypt test cases""" + handler = hash.bcrypt + reduce_default_rounds = True + fuzz_salts_need_bcrypt_repair = True + + known_correct_hashes = [ + # + # from JTR 1.7.9 + # + ('U*U*U*U*', '$2a$05$c92SVSfjeiCD6F2nAD6y0uBpJDjdRkt0EgeC4/31Rf2LUZbDRDE.O'), + ('U*U***U', '$2a$05$WY62Xk2TXZ7EvVDQ5fmjNu7b0GEzSzUXUh2cllxJwhtOeMtWV3Ujq'), + ('U*U***U*', '$2a$05$Fa0iKV3E2SYVUlMknirWU.CFYGvJ67UwVKI1E2FP6XeLiZGcH3MJi'), + ('*U*U*U*U', '$2a$05$.WRrXibc1zPgIdRXYfv.4uu6TD1KWf0VnHzq/0imhUhuxSxCyeBs2'), + ('', '$2a$05$Otz9agnajgrAe0.kFVF9V.tzaStZ2s1s4ZWi/LY4sw2k/MTVFj/IO'), + + # + # test vectors from http://www.openwall.com/crypt v1.2 + # note that this omits any hashes that depend on crypt_blowfish's + # various CVE-2011-2483 workarounds (hash 2a and \xff\xff in password, + # and any 2x hashes); and only contain hashes which are correct + # under both crypt_blowfish 1.2 AND OpenBSD. + # + ('U*U', '$2a$05$CCCCCCCCCCCCCCCCCCCCC.E5YPO9kmyuRGyh0XouQYb4YMJKvyOeW'), + ('U*U*', '$2a$05$CCCCCCCCCCCCCCCCCCCCC.VGOzA784oUp/Z0DY336zx7pLYAy0lwK'), + ('U*U*U', '$2a$05$XXXXXXXXXXXXXXXXXXXXXOAcXxm9kjPGEMsLznoKqmqw7tc8WCx4a'), + ('', '$2a$05$CCCCCCCCCCCCCCCCCCCCC.7uG0VCzI2bS7j6ymqJi9CdcdxiRTWNy'), + ('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' + '0123456789chars after 72 are ignored', + '$2a$05$abcdefghijklmnopqrstuu5s2v8.iXieOjg/.AySBTTZIIVFJeBui'), + (b'\xa3', + '$2a$05$/OK.fbVrR/bpIqNJ5ianF.Sa7shbm4.OzKpvFnX1pQLmQW96oUlCq'), + (b'\xff\xa3345', + '$2a$05$/OK.fbVrR/bpIqNJ5ianF.nRht2l/HRhr6zmCp9vYUvvsqynflf9e'), + (b'\xa3ab', + '$2a$05$/OK.fbVrR/bpIqNJ5ianF.6IflQkJytoRVc1yuaNtHfiuq.FRlSIS'), + (b'\xaa'*72 + b'chars after 72 are ignored as usual', + '$2a$05$/OK.fbVrR/bpIqNJ5ianF.swQOIzjOiJ9GHEPuhEkvqrUyvWhEMx6'), + (b'\xaa\x55'*36, + '$2a$05$/OK.fbVrR/bpIqNJ5ianF.R9xrDjiycxMbQE2bp.vgqlYpW5wx2yy'), + (b'\x55\xaa\xff'*24, + '$2a$05$/OK.fbVrR/bpIqNJ5ianF.9tQZzcJfm3uj2NvJ/n5xkhpqLrMpWCe'), + + # keeping one of their 2y tests, because we are supporting that. + (b'\xa3', + '$2y$05$/OK.fbVrR/bpIqNJ5ianF.Sa7shbm4.OzKpvFnX1pQLmQW96oUlCq'), + + # + # 8bit bug (fixed in 2y/2b) + # + + # NOTE: see assert_lacks_8bit_bug() for origins of this test vector. + (b"\xd1\x91", "$2y$05$6bNw2HLQYeqHYyBfLMsv/OUcZd0LKP39b87nBw3.S2tVZSqiQX6eu"), + + # + # bsd wraparound bug (fixed in 2b) + # + + # NOTE: if backend is vulnerable, password will hash the same as '0'*72 + # ("$2a$04$R1lJ2gkNaoPGdafE.H.16.nVyh2niHsGJhayOHLMiXlI45o8/DU.6"), + # rather than same as ("0123456789"*8)[:72] + # 255 should be sufficient, but checking + (("0123456789"*26)[:254], '$2a$04$R1lJ2gkNaoPGdafE.H.16.1MKHPvmKwryeulRe225LKProWYwt9Oi'), + (("0123456789"*26)[:255], '$2a$04$R1lJ2gkNaoPGdafE.H.16.1MKHPvmKwryeulRe225LKProWYwt9Oi'), + (("0123456789"*26)[:256], '$2a$04$R1lJ2gkNaoPGdafE.H.16.1MKHPvmKwryeulRe225LKProWYwt9Oi'), + (("0123456789"*26)[:257], '$2a$04$R1lJ2gkNaoPGdafE.H.16.1MKHPvmKwryeulRe225LKProWYwt9Oi'), + + + # + # from py-bcrypt tests + # + ('', '$2a$06$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s.'), + ('a', '$2a$10$k87L/MF28Q673VKh8/cPi.SUl7MU/rWuSiIDDFayrKk/1tBsSQu4u'), + ('abc', '$2a$10$WvvTPHKwdBJ3uk0Z37EMR.hLA2W6N9AEBhEgrAOljy2Ae5MtaSIUi'), + ('abcdefghijklmnopqrstuvwxyz', + '$2a$10$fVH8e28OQRj9tqiDXs1e1uxpsjN0c7II7YPKXua2NAKYvM6iQk7dq'), + ('~!@#$%^&*() ~!@#$%^&*()PNBFRD', + '$2a$10$LgfYWkbzEvQ4JakH7rOvHe0y8pHKF9OaFgwUZ2q7W2FFZmZzJYlfS'), + + # + # custom test vectors + # + + # ensures utf-8 used for unicode + (UPASS_TABLE, + '$2a$05$Z17AXnnlpzddNUvnC6cZNOSwMA/8oNiKnHTHTwLlBijfucQQlHjaG'), + + # ensure 2b support + (UPASS_TABLE, + '$2b$05$Z17AXnnlpzddNUvnC6cZNOSwMA/8oNiKnHTHTwLlBijfucQQlHjaG'), + + ] + + if TEST_MODE("full"): + # + # add some extra tests related to 2/2a + # + CONFIG_2 = '$2$05$' + '.'*22 + CONFIG_A = '$2a$05$' + '.'*22 + known_correct_hashes.extend([ + ("", CONFIG_2 + 'J2ihDv8vVf7QZ9BsaRrKyqs2tkn55Yq'), + ("", CONFIG_A + 'J2ihDv8vVf7QZ9BsaRrKyqs2tkn55Yq'), + ("abc", CONFIG_2 + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'), + ("abc", CONFIG_A + 'ev6gDwpVye3oMCUpLY85aTpfBNHD0Ga'), + ("abc"*23, CONFIG_2 + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'), + ("abc"*23, CONFIG_A + '2kIdfSj/4/R/Q6n847VTvc68BXiRYZC'), + ("abc"*24, CONFIG_2 + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'), + ("abc"*24, CONFIG_A + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'), + ("abc"*24+'x', CONFIG_2 + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'), + ("abc"*24+'x', CONFIG_A + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'), + ]) + + known_correct_configs = [ + ('$2a$04$uM6csdM8R9SXTex/gbTaye', UPASS_TABLE, + '$2a$04$uM6csdM8R9SXTex/gbTayezuvzFEufYGd2uB6of7qScLjQ4GwcD4G'), + ] + + known_unidentified_hashes = [ + # invalid minor version + "$2f$12$EXRkfkdmXnagzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q", + "$2`$12$EXRkfkdmXnagzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q", + ] + + known_malformed_hashes = [ + # bad char in otherwise correct hash + # \/ + "$2a$12$EXRkfkdmXn!gzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q", + + # unsupported (but recognized) minor version + "$2x$12$EXRkfkdmXnagzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q", + + # rounds not zero-padded (py-bcrypt rejects this, therefore so do we) + '$2a$6$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s.' + + # NOTE: salts with padding bits set are technically malformed, + # but we can reliably correct & issue a warning for that. + ] + + platform_crypt_support = [ + ("freedbsd|openbsd|netbsd", True), + ("darwin", False), + ("linux", None), # may be present via addon, e.g. debian's libpam-unix2 + ("solaris", None), # depends on system policy + ] + + #=================================================================== + # override some methods + #=================================================================== + def setUp(self): + # ensure builtin is enabled for duration of test. + if TEST_MODE("full") and self.backend == "builtin": + key = "PASSLIB_BUILTIN_BCRYPT" + orig = os.environ.get(key) + if orig: + self.addCleanup(os.environ.__setitem__, key, orig) + else: + self.addCleanup(os.environ.__delitem__, key) + os.environ[key] = "true" + + super(_bcrypt_test, self).setUp() + + # silence this warning, will come up a bunch during testing of old 2a hashes. + warnings.filterwarnings("ignore", ".*backend is vulnerable to the bsd wraparound bug.*") + + def populate_settings(self, kwds): + # builtin is still just way too slow. + if self.backend == "builtin": + kwds.setdefault("rounds", 4) + super(_bcrypt_test, self).populate_settings(kwds) + + #=================================================================== + # fuzz testing + #=================================================================== + def crypt_supports_variant(self, hash): + """check if OS crypt is expected to support given ident""" + from passlib.handlers.bcrypt import bcrypt, IDENT_2X, IDENT_2Y + from passlib.utils import safe_crypt + ident = bcrypt.from_string(hash) + return (safe_crypt("test", ident + "04$5BJqKfqMQvV7nS.yUguNcu") or "").startswith(ident) + + fuzz_verifiers = HandlerCase.fuzz_verifiers + ( + "fuzz_verifier_bcrypt", + "fuzz_verifier_pybcrypt", + "fuzz_verifier_bcryptor", + ) + + def fuzz_verifier_bcrypt(self): + # test against bcrypt, if available + from passlib.handlers.bcrypt import IDENT_2, IDENT_2A, IDENT_2B, IDENT_2X, IDENT_2Y, _detect_pybcrypt + from passlib.utils import to_native_str, to_bytes + try: + import bcrypt + except ImportError: + return + if _detect_pybcrypt(): + return + def check_bcrypt(secret, hash): + """bcrypt""" + secret = to_bytes(secret, self.FuzzHashGenerator.password_encoding) + if hash.startswith(IDENT_2B): + # bcrypt <1.1 lacks 2B support + hash = IDENT_2A + hash[4:] + elif hash.startswith(IDENT_2): + # bcrypt doesn't support $2$ hashes; but we can fake it + # using the $2a$ algorithm, by repeating the password until + # it's 72 chars in length. + hash = IDENT_2A + hash[3:] + if secret: + secret = repeat_string(secret, 72) + elif hash.startswith(IDENT_2Y) and bcrypt.__version__ == "3.0.0": + hash = IDENT_2B + hash[4:] + hash = to_bytes(hash) + try: + return bcrypt.hashpw(secret, hash) == hash + except ValueError: + raise ValueError("bcrypt rejected hash: %r (secret=%r)" % (hash, secret)) + return check_bcrypt + + def fuzz_verifier_pybcrypt(self): + # test against py-bcrypt, if available + from passlib.handlers.bcrypt import ( + IDENT_2, IDENT_2A, IDENT_2B, IDENT_2X, IDENT_2Y, + _PyBcryptBackend, + ) + from passlib.utils import to_native_str + + loaded = _PyBcryptBackend._load_backend_mixin("pybcrypt", False) + if not loaded: + return + + from passlib.handlers.bcrypt import _pybcrypt as bcrypt_mod + + lock = _PyBcryptBackend._calc_lock # reuse threadlock workaround for pybcrypt 0.2 + + def check_pybcrypt(secret, hash): + """pybcrypt""" + secret = to_native_str(secret, self.FuzzHashGenerator.password_encoding) + if len(secret) > 200: # vulnerable to wraparound bug + secret = secret[:200] + if hash.startswith((IDENT_2B, IDENT_2Y)): + hash = IDENT_2A + hash[4:] + try: + if lock: + with lock: + return bcrypt_mod.hashpw(secret, hash) == hash + else: + return bcrypt_mod.hashpw(secret, hash) == hash + except ValueError: + raise ValueError("py-bcrypt rejected hash: %r" % (hash,)) + return check_pybcrypt + + def fuzz_verifier_bcryptor(self): + # test against bcryptor if available + from passlib.handlers.bcrypt import IDENT_2, IDENT_2A, IDENT_2Y, IDENT_2B + from passlib.utils import to_native_str + try: + from bcryptor.engine import Engine + except ImportError: + return + def check_bcryptor(secret, hash): + """bcryptor""" + secret = to_native_str(secret, self.FuzzHashGenerator.password_encoding) + if hash.startswith((IDENT_2B, IDENT_2Y)): + hash = IDENT_2A + hash[4:] + elif hash.startswith(IDENT_2): + # bcryptor doesn't support $2$ hashes; but we can fake it + # using the $2a$ algorithm, by repeating the password until + # it's 72 chars in length. + hash = IDENT_2A + hash[3:] + if secret: + secret = repeat_string(secret, 72) + return Engine(False).hash_key(secret, hash) == hash + return check_bcryptor + + class FuzzHashGenerator(HandlerCase.FuzzHashGenerator): + + def generate(self): + opts = super(_bcrypt_test.FuzzHashGenerator, self).generate() + + secret = opts['secret'] + other = opts['other'] + settings = opts['settings'] + ident = settings.get('ident') + + if ident == IDENT_2X: + # 2x is just recognized, not supported. don't test with it. + del settings['ident'] + + elif ident == IDENT_2 and other and repeat_string(to_bytes(other), len(to_bytes(secret))) == to_bytes(secret): + # avoid false failure due to flaw in 0-revision bcrypt: + # repeated strings like 'abc' and 'abcabc' hash identically. + opts['secret'], opts['other'] = self.random_password_pair() + + return opts + + def random_rounds(self): + # decrease default rounds for fuzz testing to speed up volume. + return self.randintgauss(5, 8, 6, 1) + + #=================================================================== + # custom tests + #=================================================================== + known_incorrect_padding = [ + # password, bad hash, good hash + + # 2 bits of salt padding set +# ("loppux", # \/ +# "$2a$12$oaQbBqq8JnSM1NHRPQGXORm4GCUMqp7meTnkft4zgSnrbhoKdDV0C", +# "$2a$12$oaQbBqq8JnSM1NHRPQGXOOm4GCUMqp7meTnkft4zgSnrbhoKdDV0C"), + ("test", # \/ + '$2a$04$oaQbBqq8JnSM1NHRPQGXORY4Vw3bdHKLIXTecPDRAcJ98cz1ilveO', + '$2a$04$oaQbBqq8JnSM1NHRPQGXOOY4Vw3bdHKLIXTecPDRAcJ98cz1ilveO'), + + # all 4 bits of salt padding set +# ("Passlib11", # \/ +# "$2a$12$M8mKpW9a2vZ7PYhq/8eJVcUtKxpo6j0zAezu0G/HAMYgMkhPu4fLK", +# "$2a$12$M8mKpW9a2vZ7PYhq/8eJVOUtKxpo6j0zAezu0G/HAMYgMkhPu4fLK"), + ("test", # \/ + "$2a$04$yjDgE74RJkeqC0/1NheSScrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIS", + "$2a$04$yjDgE74RJkeqC0/1NheSSOrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIS"), + + # bad checksum padding + ("test", # \/ + "$2a$04$yjDgE74RJkeqC0/1NheSSOrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIV", + "$2a$04$yjDgE74RJkeqC0/1NheSSOrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIS"), + ] + + def test_90_bcrypt_padding(self): + """test passlib correctly handles bcrypt padding bits""" + self.require_TEST_MODE("full") + # + # prevents reccurrence of issue 25 (https://code.google.com/p/passlib/issues/detail?id=25) + # were some unused bits were incorrectly set in bcrypt salt strings. + # (fixed since 1.5.3) + # + bcrypt = self.handler + corr_desc = ".*incorrectly set padding bits" + + # + # test hash() / genconfig() don't generate invalid salts anymore + # + def check_padding(hash): + assert hash.startswith(("$2a$", "$2b$")) and len(hash) >= 28, \ + "unexpectedly malformed hash: %r" % (hash,) + self.assertTrue(hash[28] in '.Oeu', + "unused bits incorrectly set in hash: %r" % (hash,)) + for i in irange(6): + check_padding(bcrypt.genconfig()) + for i in irange(3): + check_padding(bcrypt.using(rounds=bcrypt.min_rounds).hash("bob")) + + # + # test genconfig() corrects invalid salts & issues warning. + # + with self.assertWarningList(["salt too large", corr_desc]): + hash = bcrypt.genconfig(salt="."*21 + "A.", rounds=5, relaxed=True) + self.assertEqual(hash, "$2b$05$" + "." * (22 + 31)) + + # + # test public methods against good & bad hashes + # + samples = self.known_incorrect_padding + for pwd, bad, good in samples: + + # make sure genhash() corrects bad configs, leaves good unchanged + with self.assertWarningList([corr_desc]): + self.assertEqual(bcrypt.genhash(pwd, bad), good) + with self.assertWarningList([]): + self.assertEqual(bcrypt.genhash(pwd, good), good) + + # make sure verify() works correctly with good & bad hashes + with self.assertWarningList([corr_desc]): + self.assertTrue(bcrypt.verify(pwd, bad)) + with self.assertWarningList([]): + self.assertTrue(bcrypt.verify(pwd, good)) + + # make sure normhash() corrects bad hashes, leaves good unchanged + with self.assertWarningList([corr_desc]): + self.assertEqual(bcrypt.normhash(bad), good) + with self.assertWarningList([]): + self.assertEqual(bcrypt.normhash(good), good) + + # make sure normhash() leaves non-bcrypt hashes alone + self.assertEqual(bcrypt.normhash("$md5$abc"), "$md5$abc") + + def test_needs_update_w_padding(self): + """needs_update corrects bcrypt padding""" + # NOTE: see padding test above for details about issue this detects + bcrypt = self.handler.using(rounds=4) + + # PASS1 = "test" + # bad contains invalid 'c' char at end of salt: + # \/ + BAD1 = "$2a$04$yjDgE74RJkeqC0/1NheSScrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIS" + GOOD1 = "$2a$04$yjDgE74RJkeqC0/1NheSSOrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIS" + + self.assertTrue(bcrypt.needs_update(BAD1)) + self.assertFalse(bcrypt.needs_update(GOOD1)) + + #=================================================================== + # eoc + #=================================================================== + +# create test cases for specific backends +bcrypt_bcrypt_test = _bcrypt_test.create_backend_case("bcrypt") +bcrypt_pybcrypt_test = _bcrypt_test.create_backend_case("pybcrypt") +bcrypt_bcryptor_test = _bcrypt_test.create_backend_case("bcryptor") + +class bcrypt_os_crypt_test(_bcrypt_test.create_backend_case("os_crypt")): + + # os crypt doesn't support non-utf8 secret bytes + known_correct_hashes = [row for row in _bcrypt_test.known_correct_hashes + if is_safe_crypt_input(row[0])] + + # os crypt backend doesn't currently implement a per-call fallback if it fails + has_os_crypt_fallback = False + +bcrypt_builtin_test = _bcrypt_test.create_backend_case("builtin") + +#============================================================================= +# bcrypt +#============================================================================= +class _bcrypt_sha256_test(HandlerCase): + "base for BCrypt-SHA256 test cases" + handler = hash.bcrypt_sha256 + reduce_default_rounds = True + forbidden_characters = None + fuzz_salts_need_bcrypt_repair = True + + known_correct_hashes = [ + #------------------------------------------------------------------- + # custom test vectors for old v1 format + #------------------------------------------------------------------- + + # empty + ("", + '$bcrypt-sha256$2a,5$E/e/2AOhqM5W/KJTFQzLce$F6dYSxOdAEoJZO2eoHUZWZljW/e0TXO'), + + # ascii + ("password", + '$bcrypt-sha256$2a,5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu'), + + # unicode / utf8 + (UPASS_TABLE, + '$bcrypt-sha256$2a,5$.US1fQ4TQS.ZTz/uJ5Kyn.$QNdPDOTKKT5/sovNz1iWg26quOU4Pje'), + (UPASS_TABLE.encode("utf-8"), + '$bcrypt-sha256$2a,5$.US1fQ4TQS.ZTz/uJ5Kyn.$QNdPDOTKKT5/sovNz1iWg26quOU4Pje'), + + # ensure 2b support + ("password", + '$bcrypt-sha256$2b,5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu'), + (UPASS_TABLE, + '$bcrypt-sha256$2b,5$.US1fQ4TQS.ZTz/uJ5Kyn.$QNdPDOTKKT5/sovNz1iWg26quOU4Pje'), + + # test >72 chars is hashed correctly -- under bcrypt these hash the same. + # NOTE: test_60_truncate_size() handles this already, this is just for overkill :) + (repeat_string("abc123", 72), + '$bcrypt-sha256$2b,5$X1g1nh3g0v4h6970O68cxe$r/hyEtqJ0teqPEmfTLoZ83ciAI1Q74.'), + (repeat_string("abc123", 72) + "qwr", + '$bcrypt-sha256$2b,5$X1g1nh3g0v4h6970O68cxe$021KLEif6epjot5yoxk0m8I0929ohEa'), + (repeat_string("abc123", 72) + "xyz", + '$bcrypt-sha256$2b,5$X1g1nh3g0v4h6970O68cxe$7.1kgpHduMGEjvM3fX6e/QCvfn6OKja'), + + #------------------------------------------------------------------- + # custom test vectors for v2 format + # TODO: convert to v2 format + #------------------------------------------------------------------- + + # empty + ("", + '$bcrypt-sha256$v=2,t=2b,r=5$E/e/2AOhqM5W/KJTFQzLce$WFPIZKtDDTriqWwlmRFfHiOTeheAZWe'), + + # ascii + ("password", + '$bcrypt-sha256$v=2,t=2b,r=5$5Hg1DKFqPE8C2aflZ5vVoe$wOK1VFFtS8IGTrGa7.h5fs0u84qyPbS'), + + # unicode / utf8 + (UPASS_TABLE, + '$bcrypt-sha256$v=2,t=2b,r=5$.US1fQ4TQS.ZTz/uJ5Kyn.$pzzgp40k8reM1CuQb03PvE0IDPQSdV6'), + (UPASS_TABLE.encode("utf-8"), + '$bcrypt-sha256$v=2,t=2b,r=5$.US1fQ4TQS.ZTz/uJ5Kyn.$pzzgp40k8reM1CuQb03PvE0IDPQSdV6'), + + # test >72 chars is hashed correctly -- under bcrypt these hash the same. + # NOTE: test_60_truncate_size() handles this already, this is just for overkill :) + (repeat_string("abc123", 72), + '$bcrypt-sha256$v=2,t=2b,r=5$X1g1nh3g0v4h6970O68cxe$zu1cloESVFIOsUIo7fCEgkdHaI9SSue'), + (repeat_string("abc123", 72) + "qwr", + '$bcrypt-sha256$v=2,t=2b,r=5$X1g1nh3g0v4h6970O68cxe$CBF9csfEdW68xv3DwE6xSULXMtqEFP.'), + (repeat_string("abc123", 72) + "xyz", + '$bcrypt-sha256$v=2,t=2b,r=5$X1g1nh3g0v4h6970O68cxe$zC/1UDUG2ofEXB6Onr2vvyFzfhEOS3S'), + ] + + known_correct_configs =[ + # v1 + ('$bcrypt-sha256$2a,5$5Hg1DKFqPE8C2aflZ5vVoe', + "password", '$bcrypt-sha256$2a,5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu'), + # v2 + ('$bcrypt-sha256$v=2,t=2b,r=5$5Hg1DKFqPE8C2aflZ5vVoe', + "password", '$bcrypt-sha256$v=2,t=2b,r=5$5Hg1DKFqPE8C2aflZ5vVoe$wOK1VFFtS8IGTrGa7.h5fs0u84qyPbS'), + ] + + known_malformed_hashes = [ + #------------------------------------------------------------------- + # v1 format + #------------------------------------------------------------------- + + # bad char in otherwise correct hash + # \/ + '$bcrypt-sha256$2a,5$5Hg1DKF!PE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu', + + # unrecognized bcrypt variant + '$bcrypt-sha256$2c,5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu', + + # unsupported bcrypt variant + '$bcrypt-sha256$2x,5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu', + + # rounds zero-padded + '$bcrypt-sha256$2a,05$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu', + + # config string w/ $ added + '$bcrypt-sha256$2a,5$5Hg1DKFqPE8C2aflZ5vVoe$', + + #------------------------------------------------------------------- + # v2 format + #------------------------------------------------------------------- + + # bad char in otherwise correct hash + # \/ + '$bcrypt-sha256$v=2,t=2b,r=5$5Hg1DKF!PE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu', + + # unsupported version (for this format) + '$bcrypt-sha256$v=1,t=2b,r=5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu', + + # unrecognized version + '$bcrypt-sha256$v=3,t=2b,r=5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu', + + # unrecognized bcrypt variant + '$bcrypt-sha256$v=2,t=2c,r=5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu', + + # unsupported bcrypt variant + '$bcrypt-sha256$v=2,t=2a,r=5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu', + '$bcrypt-sha256$v=2,t=2x,r=5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu', + + # rounds zero-padded + '$bcrypt-sha256$v=2,t=2b,r=05$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu', + + # config string w/ $ added + '$bcrypt-sha256$v=2,t=2b,r=5$5Hg1DKFqPE8C2aflZ5vVoe$', + ] + + #=================================================================== + # override some methods -- cloned from bcrypt + #=================================================================== + def setUp(self): + # ensure builtin is enabled for duration of test. + if TEST_MODE("full") and self.backend == "builtin": + key = "PASSLIB_BUILTIN_BCRYPT" + orig = os.environ.get(key) + if orig: + self.addCleanup(os.environ.__setitem__, key, orig) + else: + self.addCleanup(os.environ.__delitem__, key) + os.environ[key] = "enabled" + super(_bcrypt_sha256_test, self).setUp() + warnings.filterwarnings("ignore", ".*backend is vulnerable to the bsd wraparound bug.*") + + def populate_settings(self, kwds): + # builtin is still just way too slow. + if self.backend == "builtin": + kwds.setdefault("rounds", 4) + super(_bcrypt_sha256_test, self).populate_settings(kwds) + + #=================================================================== + # override ident tests for now + #=================================================================== + + def require_many_idents(self): + raise self.skipTest("multiple idents not supported") + + def test_30_HasOneIdent(self): + # forbidding ident keyword, we only support "2b" for now + handler = self.handler + handler(use_defaults=True) + self.assertRaises(ValueError, handler, ident="$2y$", use_defaults=True) + + #=================================================================== + # fuzz testing -- cloned from bcrypt + #=================================================================== + + class FuzzHashGenerator(HandlerCase.FuzzHashGenerator): + + def random_rounds(self): + # decrease default rounds for fuzz testing to speed up volume. + return self.randintgauss(5, 8, 6, 1) + + def random_ident(self): + return "2b" + + #=================================================================== + # custom tests + #=================================================================== + + def test_using_version(self): + # default to v2 + handler = self.handler + self.assertEqual(handler.version, 2) + + # allow v1 explicitly + subcls = handler.using(version=1) + self.assertEqual(subcls.version, 1) + + # forbid unknown ver + self.assertRaises(ValueError, handler.using, version=999) + + # allow '2a' only for v1 + subcls = handler.using(version=1, ident="2a") + self.assertRaises(ValueError, handler.using, ident="2a") + + def test_calc_digest_v2(self): + """ + test digest calc v2 matches bcrypt() + """ + from passlib.hash import bcrypt + from passlib.crypto.digest import compile_hmac + from passlib.utils.binary import b64encode + + # manually calc intermediary digest + salt = "nyKYxTAvjmy6lMDYMl11Uu" + secret = "test" + temp_digest = compile_hmac("sha256", salt.encode("ascii"))(secret.encode("ascii")) + temp_digest = b64encode(temp_digest).decode("ascii") + self.assertEqual(temp_digest, "J5TlyIDm+IcSWmKiDJm+MeICndBkFVPn4kKdJW8f+xY=") + + # manually final hash from intermediary + # XXX: genhash() could be useful here + bcrypt_digest = bcrypt(ident="2b", salt=salt, rounds=12)._calc_checksum(temp_digest) + self.assertEqual(bcrypt_digest, "M0wE0Ov/9LXoQFCe.jRHu3MSHPF54Ta") + self.assertTrue(bcrypt.verify(temp_digest, "$2b$12$" + salt + bcrypt_digest)) + + # confirm handler outputs same thing. + # XXX: genhash() could be useful here + result = self.handler(ident="2b", salt=salt, rounds=12)._calc_checksum(secret) + self.assertEqual(result, bcrypt_digest) + + #=================================================================== + # eoc + #=================================================================== + +# create test cases for specific backends +bcrypt_sha256_bcrypt_test = _bcrypt_sha256_test.create_backend_case("bcrypt") +bcrypt_sha256_pybcrypt_test = _bcrypt_sha256_test.create_backend_case("pybcrypt") +bcrypt_sha256_bcryptor_test = _bcrypt_sha256_test.create_backend_case("bcryptor") + +class bcrypt_sha256_os_crypt_test(_bcrypt_sha256_test.create_backend_case("os_crypt")): + + @classmethod + def _get_safe_crypt_handler_backend(cls): + return bcrypt_os_crypt_test._get_safe_crypt_handler_backend() + + has_os_crypt_fallback = False + +bcrypt_sha256_builtin_test = _bcrypt_sha256_test.create_backend_case("builtin") + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/test_handlers_cisco.py b/ansible/lib/python3.11/site-packages/passlib/tests/test_handlers_cisco.py new file mode 100644 index 000000000..ea6594bf8 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/tests/test_handlers_cisco.py @@ -0,0 +1,457 @@ +""" +passlib.tests.test_handlers_cisco - tests for Cisco-specific algorithms +""" +#============================================================================= +# imports +#============================================================================= +from __future__ import absolute_import, division, print_function +# core +import logging +log = logging.getLogger(__name__) +# site +# pkg +from passlib import hash, exc +from passlib.utils.compat import u +from .utils import UserHandlerMixin, HandlerCase, repeat_string +from .test_handlers import UPASS_TABLE +# module +__all__ = [ + "cisco_pix_test", + "cisco_asa_test", + "cisco_type7_test", +] +#============================================================================= +# shared code for cisco PIX & ASA +#============================================================================= + +class _PixAsaSharedTest(UserHandlerMixin, HandlerCase): + """ + class w/ shared info for PIX & ASA tests. + """ + __unittest_skip = True # for TestCase + requires_user = False # for UserHandlerMixin + + #: shared list of hashes which should be identical under pix & asa7 + #: (i.e. combined secret + user < 17 bytes) + pix_asa_shared_hashes = [ + # + # http://www.perlmonks.org/index.pl?node_id=797623 + # + (("cisco", ""), "2KFQnbNIdI.2KYOU"), # confirmed ASA 9.6 + + # + # http://www.hsc.fr/ressources/breves/pix_crack.html.en + # + (("hsc", ""), "YtT8/k6Np8F1yz2c"), # confirmed ASA 9.6 + + # + # www.freerainbowtables.com/phpBB3/viewtopic.php?f=2&t=1441 + # + (("", ""), "8Ry2YjIyt7RRXU24"), # confirmed ASA 9.6 + (("cisco", "john"), "hN7LzeyYjw12FSIU"), + (("cisco", "jack"), "7DrfeZ7cyOj/PslD"), + + # + # http://comments.gmane.org/gmane.comp.security.openwall.john.user/2529 + # + (("ripper", "alex"), "h3mJrcH0901pqX/m"), + (("cisco", "cisco"), "3USUcOPFUiMCO4Jk"), + (("cisco", "cisco1"), "3USUcOPFUiMCO4Jk"), + (("CscFw-ITC!", "admcom"), "lZt7HSIXw3.QP7.R"), + ("cangetin", "TynyB./ftknE77QP"), + (("cangetin", "rramsey"), "jgBZqYtsWfGcUKDi"), + + # + # http://openwall.info/wiki/john/sample-hashes + # + (("phonehome", "rharris"), "zyIIMSYjiPm0L7a6"), + + # + # http://www.openwall.com/lists/john-users/2010/08/08/3 + # + (("cangetin", ""), "TynyB./ftknE77QP"), + (("cangetin", "rramsey"), "jgBZqYtsWfGcUKDi"), + + # + # from JTR 1.7.9 + # + ("test1", "TRPEas6f/aa6JSPL"), + ("test2", "OMT6mXmAvGyzrCtp"), + ("test3", "gTC7RIy1XJzagmLm"), + ("test4", "oWC1WRwqlBlbpf/O"), + ("password", "NuLKvvWGg.x9HEKO"), + ("0123456789abcdef", ".7nfVBEIEu4KbF/1"), + + # + # http://www.cisco.com/en/US/docs/security/pix/pix50/configuration/guide/commands.html#wp5472 + # + (("1234567890123456", ""), "feCkwUGktTCAgIbD"), # canonical source + (("watag00s1am", ""), "jMorNbK0514fadBh"), # canonical source + + # + # custom + # + (("cisco1", "cisco1"), "jmINXNH6p1BxUppp"), + + # ensures utf-8 used for unicode + (UPASS_TABLE, 'CaiIvkLMu2TOHXGT'), + + # + # passlib reference vectors + # + # Some of these have been confirmed on various ASA firewalls, + # and the exact version is noted next to each hash. + # Would like to verify these under more PIX & ASA versions. + # + # Those without a note are generally an extrapolation, + # to ensure the code stays consistent, but for various reasons, + # hasn't been verified. + # + # * One such case is usernames w/ 1 & 2 digits -- + # ASA (9.6 at least) requires 3+ digits in username. + # + # The following hashes (below 13 chars) should be identical for PIX/ASA. + # Ones which differ are listed separately in the known_correct_hashes + # list for the two test classes. + # + + # 4 char password + (('1234', ''), 'RLPMUQ26KL4blgFN'), # confirmed ASA 9.6 + + # 8 char password + (('01234567', ''), '0T52THgnYdV1tlOF'), # confirmed ASA 9.6 + (('01234567', '3'), '.z0dT9Alkdc7EIGS'), + (('01234567', '36'), 'CC3Lam53t/mHhoE7'), + (('01234567', '365'), '8xPrWpNnBdD2DzdZ'), # confirmed ASA 9.6 + (('01234567', '3333'), '.z0dT9Alkdc7EIGS'), # confirmed ASA 9.6 + (('01234567', '3636'), 'CC3Lam53t/mHhoE7'), # confirmed ASA 9.6 + (('01234567', '3653'), '8xPrWpNnBdD2DzdZ'), # confirmed ASA 9.6 + (('01234567', 'adm'), 'dfWs2qiao6KD/P2L'), # confirmed ASA 9.6 + (('01234567', 'adma'), 'dfWs2qiao6KD/P2L'), # confirmed ASA 9.6 + (('01234567', 'admad'), 'dfWs2qiao6KD/P2L'), # confirmed ASA 9.6 + (('01234567', 'user'), 'PNZ4ycbbZ0jp1.j1'), # confirmed ASA 9.6 + (('01234567', 'user1234'), 'PNZ4ycbbZ0jp1.j1'), # confirmed ASA 9.6 + + # 12 char password + (('0123456789ab', ''), 'S31BxZOGlAigndcJ'), # confirmed ASA 9.6 + (('0123456789ab', '36'), 'wFqSX91X5.YaRKsi'), + (('0123456789ab', '365'), 'qjgo3kNgTVxExbno'), # confirmed ASA 9.6 + (('0123456789ab', '3333'), 'mcXPL/vIZcIxLUQs'), # confirmed ASA 9.6 + (('0123456789ab', '3636'), 'wFqSX91X5.YaRKsi'), # confirmed ASA 9.6 + (('0123456789ab', '3653'), 'qjgo3kNgTVxExbno'), # confirmed ASA 9.6 + (('0123456789ab', 'user'), 'f.T4BKdzdNkjxQl7'), # confirmed ASA 9.6 + (('0123456789ab', 'user1234'), 'f.T4BKdzdNkjxQl7'), # confirmed ASA 9.6 + + # NOTE: remaining reference vectors for 13+ char passwords + # are split up between cisco_pix & cisco_asa tests. + + # unicode passwords + # ASA supposedly uses utf-8 encoding, but entering non-ascii + # chars is error-prone, and while UTF-8 appears to be intended, + # observed behaviors include: + # * ssh cli stripping non-ascii chars entirely + # * ASDM web iface double-encoding utf-8 strings + ((u("t\xe1ble").encode("utf-8"), 'user'), 'Og8fB4NyF0m5Ed9c'), + ((u("t\xe1ble").encode("utf-8").decode("latin-1").encode("utf-8"), + 'user'), 'cMvFC2XVBmK/68yB'), # confirmed ASA 9.6 when typed into ASDM + ] + + def test_calc_digest_spoiler(self): + """ + _calc_checksum() -- spoil oversize passwords during verify + + for details, see 'spoil_digest' flag instead that function. + this helps cisco_pix/cisco_asa implement their policy of + ``.truncate_verify_reject=True``. + """ + def calc(secret, for_hash=False): + return self.handler(use_defaults=for_hash)._calc_checksum(secret) + + # short (non-truncated) password + short_secret = repeat_string("1234", self.handler.truncate_size) + short_hash = calc(short_secret) + + # longer password should have totally different hash, + # to prevent verify from matching (i.e. "spoiled"). + long_secret = short_secret + "X" + long_hash = calc(long_secret) + self.assertNotEqual(long_hash, short_hash) + + # spoiled hash should depend on whole secret, + # so that output isn't predictable + alt_long_secret = short_secret + "Y" + alt_long_hash = calc(alt_long_secret) + self.assertNotEqual(alt_long_hash, short_hash) + self.assertNotEqual(alt_long_hash, long_hash) + + # for hash(), should throw error if password too large + calc(short_secret, for_hash=True) + self.assertRaises(exc.PasswordSizeError, calc, long_secret, for_hash=True) + self.assertRaises(exc.PasswordSizeError, calc, alt_long_secret, for_hash=True) + +#============================================================================= +# cisco pix +#============================================================================= +class cisco_pix_test(_PixAsaSharedTest): + handler = hash.cisco_pix + + #: known correct pix hashes + known_correct_hashes = _PixAsaSharedTest.pix_asa_shared_hashes + [ + # + # passlib reference vectors (PIX-specific) + # + # NOTE: See 'pix_asa_shared_hashes' for general PIX+ASA vectors, + # and general notes about the 'passlib reference vectors' test set. + # + # All of the following are PIX-specific, as ASA starts + # to use a different padding size at 13 characters. + # + # TODO: these need confirming w/ an actual PIX system. + # + + # 13 char password + (('0123456789abc', ''), 'eacOpB7vE7ZDukSF'), + (('0123456789abc', '3'), 'ylJTd/qei66WZe3w'), + (('0123456789abc', '36'), 'hDx8QRlUhwd6bU8N'), + (('0123456789abc', '365'), 'vYOOtnkh1HXcMrM7'), + (('0123456789abc', '3333'), 'ylJTd/qei66WZe3w'), + (('0123456789abc', '3636'), 'hDx8QRlUhwd6bU8N'), + (('0123456789abc', '3653'), 'vYOOtnkh1HXcMrM7'), + (('0123456789abc', 'user'), 'f4/.SALxqDo59mfV'), + (('0123456789abc', 'user1234'), 'f4/.SALxqDo59mfV'), + + # 14 char password + (('0123456789abcd', ''), '6r8888iMxEoPdLp4'), + (('0123456789abcd', '3'), 'f5lvmqWYj9gJqkIH'), + (('0123456789abcd', '36'), 'OJJ1Khg5HeAYBH1c'), + (('0123456789abcd', '365'), 'OJJ1Khg5HeAYBH1c'), + (('0123456789abcd', '3333'), 'f5lvmqWYj9gJqkIH'), + (('0123456789abcd', '3636'), 'OJJ1Khg5HeAYBH1c'), + (('0123456789abcd', '3653'), 'OJJ1Khg5HeAYBH1c'), + (('0123456789abcd', 'adm'), 'DbPLCFIkHc2SiyDk'), + (('0123456789abcd', 'adma'), 'DbPLCFIkHc2SiyDk'), + (('0123456789abcd', 'user'), 'WfO2UiTapPkF/FSn'), + (('0123456789abcd', 'user1234'), 'WfO2UiTapPkF/FSn'), + + # 15 char password + (('0123456789abcde', ''), 'al1e0XFIugTYLai3'), + (('0123456789abcde', '3'), 'lYbwBu.f82OIApQB'), + (('0123456789abcde', '36'), 'lYbwBu.f82OIApQB'), + (('0123456789abcde', '365'), 'lYbwBu.f82OIApQB'), + (('0123456789abcde', '3333'), 'lYbwBu.f82OIApQB'), + (('0123456789abcde', '3636'), 'lYbwBu.f82OIApQB'), + (('0123456789abcde', '3653'), 'lYbwBu.f82OIApQB'), + (('0123456789abcde', 'adm'), 'KgKx1UQvdR/09i9u'), + (('0123456789abcde', 'adma'), 'KgKx1UQvdR/09i9u'), + (('0123456789abcde', 'user'), 'qLopkenJ4WBqxaZN'), + (('0123456789abcde', 'user1234'), 'qLopkenJ4WBqxaZN'), + + # 16 char password + (('0123456789abcdef', ''), '.7nfVBEIEu4KbF/1'), + (('0123456789abcdef', '36'), '.7nfVBEIEu4KbF/1'), + (('0123456789abcdef', '365'), '.7nfVBEIEu4KbF/1'), + (('0123456789abcdef', '3333'), '.7nfVBEIEu4KbF/1'), + (('0123456789abcdef', '3636'), '.7nfVBEIEu4KbF/1'), + (('0123456789abcdef', '3653'), '.7nfVBEIEu4KbF/1'), + (('0123456789abcdef', 'user'), '.7nfVBEIEu4KbF/1'), + (('0123456789abcdef', 'user1234'), '.7nfVBEIEu4KbF/1'), + ] + + +#============================================================================= +# cisco asa +#============================================================================= +class cisco_asa_test(_PixAsaSharedTest): + handler = hash.cisco_asa + + known_correct_hashes = _PixAsaSharedTest.pix_asa_shared_hashes + [ + # + # passlib reference vectors (ASA-specific) + # + # NOTE: See 'pix_asa_shared_hashes' for general PIX+ASA vectors, + # and general notes about the 'passlib reference vectors' test set. + # + + # 13 char password + # NOTE: past this point, ASA pads to 32 bytes instead of 16 + # for all cases where user is set (secret + 4 bytes > 16), + # but still uses 16 bytes for enable pwds (secret <= 16). + # hashes w/ user WON'T match PIX, but "enable" passwords will. + (('0123456789abc', ''), 'eacOpB7vE7ZDukSF'), # confirmed ASA 9.6 + (('0123456789abc', '36'), 'FRV9JG18UBEgX0.O'), + (('0123456789abc', '365'), 'NIwkusG9hmmMy6ZQ'), # confirmed ASA 9.6 + (('0123456789abc', '3333'), 'NmrkP98nT7RAeKZz'), # confirmed ASA 9.6 + (('0123456789abc', '3636'), 'FRV9JG18UBEgX0.O'), # confirmed ASA 9.6 + (('0123456789abc', '3653'), 'NIwkusG9hmmMy6ZQ'), # confirmed ASA 9.6 + (('0123456789abc', 'user'), '8Q/FZeam5ai1A47p'), # confirmed ASA 9.6 + (('0123456789abc', 'user1234'), '8Q/FZeam5ai1A47p'), # confirmed ASA 9.6 + + # 14 char password + (('0123456789abcd', ''), '6r8888iMxEoPdLp4'), # confirmed ASA 9.6 + (('0123456789abcd', '3'), 'yxGoujXKPduTVaYB'), + (('0123456789abcd', '36'), 'W0jckhnhjnr/DiT/'), + (('0123456789abcd', '365'), 'HuVOxfMQNahaoF8u'), # confirmed ASA 9.6 + (('0123456789abcd', '3333'), 'yxGoujXKPduTVaYB'), # confirmed ASA 9.6 + (('0123456789abcd', '3636'), 'W0jckhnhjnr/DiT/'), # confirmed ASA 9.6 + (('0123456789abcd', '3653'), 'HuVOxfMQNahaoF8u'), # confirmed ASA 9.6 + (('0123456789abcd', 'adm'), 'RtOmSeoCs4AUdZqZ'), # confirmed ASA 9.6 + (('0123456789abcd', 'adma'), 'RtOmSeoCs4AUdZqZ'), # confirmed ASA 9.6 + (('0123456789abcd', 'user'), 'rrucwrcM0h25pr.m'), # confirmed ASA 9.6 + (('0123456789abcd', 'user1234'), 'rrucwrcM0h25pr.m'), # confirmed ASA 9.6 + + # 15 char password + (('0123456789abcde', ''), 'al1e0XFIugTYLai3'), # confirmed ASA 9.6 + (('0123456789abcde', '3'), 'nAZrQoHaL.fgrIqt'), + (('0123456789abcde', '36'), '2GxIQ6ICE795587X'), + (('0123456789abcde', '365'), 'QmDsGwCRBbtGEKqM'), # confirmed ASA 9.6 + (('0123456789abcde', '3333'), 'nAZrQoHaL.fgrIqt'), # confirmed ASA 9.6 + (('0123456789abcde', '3636'), '2GxIQ6ICE795587X'), # confirmed ASA 9.6 + (('0123456789abcde', '3653'), 'QmDsGwCRBbtGEKqM'), # confirmed ASA 9.6 + (('0123456789abcde', 'adm'), 'Aj2aP0d.nk62wl4m'), # confirmed ASA 9.6 + (('0123456789abcde', 'adma'), 'Aj2aP0d.nk62wl4m'), # confirmed ASA 9.6 + (('0123456789abcde', 'user'), 'etxiXfo.bINJcXI7'), # confirmed ASA 9.6 + (('0123456789abcde', 'user1234'), 'etxiXfo.bINJcXI7'), # confirmed ASA 9.6 + + # 16 char password + (('0123456789abcdef', ''), '.7nfVBEIEu4KbF/1'), # confirmed ASA 9.6 + (('0123456789abcdef', '36'), 'GhI8.yFSC5lwoafg'), + (('0123456789abcdef', '365'), 'KFBI6cNQauyY6h/G'), # confirmed ASA 9.6 + (('0123456789abcdef', '3333'), 'Ghdi1IlsswgYzzMH'), # confirmed ASA 9.6 + (('0123456789abcdef', '3636'), 'GhI8.yFSC5lwoafg'), # confirmed ASA 9.6 + (('0123456789abcdef', '3653'), 'KFBI6cNQauyY6h/G'), # confirmed ASA 9.6 + (('0123456789abcdef', 'user'), 'IneB.wc9sfRzLPoh'), # confirmed ASA 9.6 + (('0123456789abcdef', 'user1234'), 'IneB.wc9sfRzLPoh'), # confirmed ASA 9.6 + + # 17 char password + # NOTE: past this point, ASA pads to 32 bytes instead of 16 + # for ALL cases, since secret > 16 bytes even for enable pwds; + # and so none of these rest here should match PIX. + (('0123456789abcdefq', ''), 'bKshl.EN.X3CVFRQ'), # confirmed ASA 9.6 + (('0123456789abcdefq', '36'), 'JAeTXHs0n30svlaG'), + (('0123456789abcdefq', '365'), '4fKSSUBHT1ChGqHp'), # confirmed ASA 9.6 + (('0123456789abcdefq', '3333'), 'USEJbxI6.VY4ecBP'), # confirmed ASA 9.6 + (('0123456789abcdefq', '3636'), 'JAeTXHs0n30svlaG'), # confirmed ASA 9.6 + (('0123456789abcdefq', '3653'), '4fKSSUBHT1ChGqHp'), # confirmed ASA 9.6 + (('0123456789abcdefq', 'user'), '/dwqyD7nGdwSrDwk'), # confirmed ASA 9.6 + (('0123456789abcdefq', 'user1234'), '/dwqyD7nGdwSrDwk'), # confirmed ASA 9.6 + + # 27 char password + (('0123456789abcdefqwertyuiopa', ''), '4wp19zS3OCe.2jt5'), # confirmed ASA 9.6 + (('0123456789abcdefqwertyuiopa', '36'), 'PjUoGqWBKPyV9qOe'), + (('0123456789abcdefqwertyuiopa', '365'), 'bfCy6xFAe5O/gzvM'), # confirmed ASA 9.6 + (('0123456789abcdefqwertyuiopa', '3333'), 'rd/ZMuGTJFIb2BNG'), # confirmed ASA 9.6 + (('0123456789abcdefqwertyuiopa', '3636'), 'PjUoGqWBKPyV9qOe'), # confirmed ASA 9.6 + (('0123456789abcdefqwertyuiopa', '3653'), 'bfCy6xFAe5O/gzvM'), # confirmed ASA 9.6 + (('0123456789abcdefqwertyuiopa', 'user'), 'zynfWw3UtszxLMgL'), # confirmed ASA 9.6 + (('0123456789abcdefqwertyuiopa', 'user1234'), 'zynfWw3UtszxLMgL'), # confirmed ASA 9.6 + + # 28 char password + # NOTE: past this point, ASA stops appending the username AT ALL, + # even though there's still room for the first few chars. + (('0123456789abcdefqwertyuiopas', ''), 'W6nbOddI0SutTK7m'), # confirmed ASA 9.6 + (('0123456789abcdefqwertyuiopas', '36'), 'W6nbOddI0SutTK7m'), + (('0123456789abcdefqwertyuiopas', '365'), 'W6nbOddI0SutTK7m'), # confirmed ASA 9.6 + (('0123456789abcdefqwertyuiopas', 'user'), 'W6nbOddI0SutTK7m'), # confirmed ASA 9.6 + (('0123456789abcdefqwertyuiopas', 'user1234'), 'W6nbOddI0SutTK7m'), # confirmed ASA 9.6 + + # 32 char password + # NOTE: this is max size that ASA allows, and throws error for larger + (('0123456789abcdefqwertyuiopasdfgh', ''), '5hPT/iC6DnoBxo6a'), # confirmed ASA 9.6 + (('0123456789abcdefqwertyuiopasdfgh', '36'), '5hPT/iC6DnoBxo6a'), + (('0123456789abcdefqwertyuiopasdfgh', '365'), '5hPT/iC6DnoBxo6a'), # confirmed ASA 9.6 + (('0123456789abcdefqwertyuiopasdfgh', 'user'), '5hPT/iC6DnoBxo6a'), # confirmed ASA 9.6 + (('0123456789abcdefqwertyuiopasdfgh', 'user1234'), '5hPT/iC6DnoBxo6a'), # confirmed ASA 9.6 + ] + + +#============================================================================= +# cisco type 7 +#============================================================================= +class cisco_type7_test(HandlerCase): + handler = hash.cisco_type7 + salt_bits = 4 + salt_type = int + + known_correct_hashes = [ + # + # http://mccltd.net/blog/?p=1034 + # + ("secure ", "04480E051A33490E"), + + # + # http://insecure.org/sploits/cisco.passwords.html + # + ("Its time to go to lunch!", + "153B1F1F443E22292D73212D5300194315591954465A0D0B59"), + + # + # http://blog.ioshints.info/2007/11/type-7-decryption-in-cisco-ios.html + # + ("t35t:pa55w0rd", "08351F1B1D431516475E1B54382F"), + + # + # http://www.m00nie.com/2011/09/cisco-type-7-password-decryption-and-encryption-with-perl/ + # + ("hiImTesting:)", "020E0D7206320A325847071E5F5E"), + + # + # http://packetlife.net/forums/thread/54/ + # + ("cisco123", "060506324F41584B56"), + ("cisco123", "1511021F07257A767B"), + + # + # source ? + # + ('Supe&8ZUbeRp4SS', "06351A3149085123301517391C501918"), + + # + # custom + # + + # ensures utf-8 used for unicode + (UPASS_TABLE, '0958EDC8A9F495F6F8A5FD'), + ] + + known_unidentified_hashes = [ + # salt with hex value + "0A480E051A33490E", + + # salt value > 52. this may in fact be valid, but we reject it for now + # (see docs for more). + '99400E4812', + ] + + def test_90_decode(self): + """test cisco_type7.decode()""" + from passlib.utils import to_unicode, to_bytes + + handler = self.handler + for secret, hash in self.known_correct_hashes: + usecret = to_unicode(secret) + bsecret = to_bytes(secret) + self.assertEqual(handler.decode(hash), usecret) + self.assertEqual(handler.decode(hash, None), bsecret) + + self.assertRaises(UnicodeDecodeError, handler.decode, + '0958EDC8A9F495F6F8A5FD', 'ascii') + + def test_91_salt(self): + """test salt value border cases""" + handler = self.handler + self.assertRaises(TypeError, handler, salt=None) + handler(salt=None, use_defaults=True) + self.assertRaises(TypeError, handler, salt='abc') + self.assertRaises(ValueError, handler, salt=-10) + self.assertRaises(ValueError, handler, salt=100) + + self.assertRaises(TypeError, handler.using, salt='abc') + self.assertRaises(ValueError, handler.using, salt=-10) + self.assertRaises(ValueError, handler.using, salt=100) + with self.assertWarningList("salt/offset must be.*"): + subcls = handler.using(salt=100, relaxed=True) + self.assertEqual(subcls(use_defaults=True).salt, 52) + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/test_handlers_django.py b/ansible/lib/python3.11/site-packages/passlib/tests/test_handlers_django.py new file mode 100644 index 000000000..f7c9a0d8f --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/tests/test_handlers_django.py @@ -0,0 +1,413 @@ +"""passlib.tests.test_handlers_django - tests for passlib hash algorithms""" +#============================================================================= +# imports +#============================================================================= +from __future__ import with_statement +# core +import logging; log = logging.getLogger(__name__) +import re +import warnings +# site +# pkg +from passlib import hash +from passlib.utils import repeat_string +from passlib.utils.compat import u +from passlib.tests.utils import TestCase, HandlerCase, skipUnless, SkipTest +from passlib.tests.test_handlers import UPASS_USD, UPASS_TABLE +from passlib.tests.test_ext_django import DJANGO_VERSION, MIN_DJANGO_VERSION, \ + check_django_hasher_has_backend +# module + +#============================================================================= +# django +#============================================================================= + +# standard string django uses +UPASS_LETMEIN = u('l\xe8tmein') + +def vstr(version): + return ".".join(str(e) for e in version) + +class _DjangoHelper(TestCase): + """ + mixin for HandlerCase subclasses that are testing a hasher + which is also present in django. + """ + __unittest_skip = True + + #: minimum django version where hash alg is present / that we support testing against + min_django_version = MIN_DJANGO_VERSION + + #: max django version where hash alg is present + #: TODO: for a bunch of the tests below, this is just max version where + #: settings.PASSWORD_HASHERS includes it by default -- could add helper to patch + #: desired django hasher back in for duration of test. + #: XXX: change this to "disabled_in_django_version" instead? + max_django_version = None + + def _require_django_support(self): + # make sure min django version + if DJANGO_VERSION < self.min_django_version: + raise self.skipTest("Django >= %s not installed" % vstr(self.min_django_version)) + if self.max_django_version and DJANGO_VERSION > self.max_django_version: + raise self.skipTest("Django <= %s not installed" % vstr(self.max_django_version)) + + # make sure django has a backend for specified hasher + name = self.handler.django_name + if not check_django_hasher_has_backend(name): + raise self.skipTest('django hasher %r not available' % name) + + return True + + extra_fuzz_verifiers = HandlerCase.fuzz_verifiers + ( + "fuzz_verifier_django", + ) + + def fuzz_verifier_django(self): + try: + self._require_django_support() + except SkipTest: + return None + from django.contrib.auth.hashers import check_password + + def verify_django(secret, hash): + """django/check_password""" + if self.handler.name == "django_bcrypt" and hash.startswith("bcrypt$$2y$"): + hash = hash.replace("$$2y$", "$$2a$") + if isinstance(secret, bytes): + secret = secret.decode("utf-8") + return check_password(secret, hash) + return verify_django + + def test_90_django_reference(self): + """run known correct hashes through Django's check_password()""" + self._require_django_support() + # XXX: esp. when it's no longer supported by django, + # should verify it's *NOT* recognized + from django.contrib.auth.hashers import check_password + assert self.known_correct_hashes + for secret, hash in self.iter_known_hashes(): + self.assertTrue(check_password(secret, hash), + "secret=%r hash=%r failed to verify" % + (secret, hash)) + self.assertFalse(check_password('x' + secret, hash), + "mangled secret=%r hash=%r incorrect verified" % + (secret, hash)) + + def test_91_django_generation(self): + """test against output of Django's make_password()""" + self._require_django_support() + # XXX: esp. when it's no longer supported by django, + # should verify it's *NOT* recognized + from passlib.utils import tick + from django.contrib.auth.hashers import make_password + name = self.handler.django_name # set for all the django_* handlers + end = tick() + self.max_fuzz_time/2 + generator = self.FuzzHashGenerator(self, self.getRandom()) + while tick() < end: + secret, other = generator.random_password_pair() + if not secret: # django rejects empty passwords. + continue + if isinstance(secret, bytes): + secret = secret.decode("utf-8") + hash = make_password(secret, hasher=name) + self.assertTrue(self.do_identify(hash)) + self.assertTrue(self.do_verify(secret, hash)) + self.assertFalse(self.do_verify(other, hash)) + +class django_disabled_test(HandlerCase): + """test django_disabled""" + handler = hash.django_disabled + disabled_contains_salt = True + + known_correct_hashes = [ + # *everything* should hash to "!", and nothing should verify + ("password", "!"), + ("", "!"), + (UPASS_TABLE, "!"), + ] + + known_alternate_hashes = [ + # django 1.6 appends random alpnum string + ("!9wa845vn7098ythaehasldkfj", "password", "!"), + ] + +class django_des_crypt_test(HandlerCase, _DjangoHelper): + """test django_des_crypt""" + handler = hash.django_des_crypt + max_django_version = (1,9) + + known_correct_hashes = [ + # ensures only first two digits of salt count. + ("password", 'crypt$c2$c2M87q...WWcU'), + ("password", 'crypt$c2e86$c2M87q...WWcU'), + ("passwordignoreme", 'crypt$c2.AZ$c2M87q...WWcU'), + + # ensures utf-8 used for unicode + (UPASS_USD, 'crypt$c2e86$c2hN1Bxd6ZiWs'), + (UPASS_TABLE, 'crypt$0.aQs$0.wB.TT0Czvlo'), + (u("hell\u00D6"), "crypt$sa$saykDgk3BPZ9E"), + + # prevent regression of issue 22 + ("foo", 'crypt$MNVY.9ajgdvDQ$MNVY.9ajgdvDQ'), + ] + + known_alternate_hashes = [ + # ensure django 1.4 empty salt field is accepted; + # but that salt field is re-filled (for django 1.0 compatibility) + ('crypt$$c2M87q...WWcU', "password", 'crypt$c2$c2M87q...WWcU'), + ] + + known_unidentified_hashes = [ + 'sha1$aa$bb', + ] + + known_malformed_hashes = [ + # checksum too short + 'crypt$c2$c2M87q', + + # salt must be >2 + 'crypt$f$c2M87q...WWcU', + + # make sure first 2 chars of salt & chk field agree. + 'crypt$ffe86$c2M87q...WWcU', + ] + +class django_salted_md5_test(HandlerCase, _DjangoHelper): + """test django_salted_md5""" + handler = hash.django_salted_md5 + max_django_version = (1,9) + + known_correct_hashes = [ + # test extra large salt + ("password", 'md5$123abcdef$c8272612932975ee80e8a35995708e80'), + + # test django 1.4 alphanumeric salt + ("test", 'md5$3OpqnFAHW5CT$54b29300675271049a1ebae07b395e20'), + + # ensures utf-8 used for unicode + (UPASS_USD, 'md5$c2e86$92105508419a81a6babfaecf876a2fa0'), + (UPASS_TABLE, 'md5$d9eb8$01495b32852bffb27cf5d4394fe7a54c'), + ] + + known_unidentified_hashes = [ + 'sha1$aa$bb', + ] + + known_malformed_hashes = [ + # checksum too short + 'md5$aa$bb', + ] + + class FuzzHashGenerator(HandlerCase.FuzzHashGenerator): + + def random_salt_size(self): + # workaround for django14 regression -- + # 1.4 won't accept hashes with empty salt strings, unlike 1.3 and earlier. + # looks to be fixed in a future release -- https://code.djangoproject.com/ticket/18144 + # for now, we avoid salt_size==0 under 1.4 + handler = self.handler + default = handler.default_salt_size + assert handler.min_salt_size == 0 + lower = 1 + upper = handler.max_salt_size or default*4 + return self.randintgauss(lower, upper, default, default*.5) + +class django_salted_sha1_test(HandlerCase, _DjangoHelper): + """test django_salted_sha1""" + handler = hash.django_salted_sha1 + max_django_version = (1,9) + + known_correct_hashes = [ + # test extra large salt + ("password",'sha1$123abcdef$e4a1877b0e35c47329e7ed7e58014276168a37ba'), + + # test django 1.4 alphanumeric salt + ("test", 'sha1$bcwHF9Hy8lxS$6b4cfa0651b43161c6f1471ce9523acf1f751ba3'), + + # ensures utf-8 used for unicode + (UPASS_USD, 'sha1$c2e86$0f75c5d7fbd100d587c127ef0b693cde611b4ada'), + (UPASS_TABLE, 'sha1$6d853$ef13a4d8fb57aed0cb573fe9c82e28dc7fd372d4'), + + # generic password + ("MyPassword", 'sha1$54123$893cf12e134c3c215f3a76bd50d13f92404a54d3'), + ] + + known_unidentified_hashes = [ + 'md5$aa$bb', + ] + + known_malformed_hashes = [ + # checksum too short + 'sha1$c2e86$0f75', + ] + + # reuse custom random_salt_size() helper... + FuzzHashGenerator = django_salted_md5_test.FuzzHashGenerator + +class django_pbkdf2_sha256_test(HandlerCase, _DjangoHelper): + """test django_pbkdf2_sha256""" + handler = hash.django_pbkdf2_sha256 + + known_correct_hashes = [ + # + # custom - generated via django 1.4 hasher + # + ('not a password', + 'pbkdf2_sha256$10000$kjVJaVz6qsnJ$5yPHw3rwJGECpUf70daLGhOrQ5+AMxIJdz1c3bqK1Rs='), + (UPASS_TABLE, + 'pbkdf2_sha256$10000$bEwAfNrH1TlQ$OgYUblFNUX1B8GfMqaCYUK/iHyO0pa7STTDdaEJBuY0='), + ] + +class django_pbkdf2_sha1_test(HandlerCase, _DjangoHelper): + """test django_pbkdf2_sha1""" + handler = hash.django_pbkdf2_sha1 + + known_correct_hashes = [ + # + # custom - generated via django 1.4 hashers + # + ('not a password', + 'pbkdf2_sha1$10000$wz5B6WkasRoF$atJmJ1o+XfJxKq1+Nu1f1i57Z5I='), + (UPASS_TABLE, + 'pbkdf2_sha1$10000$KZKWwvqb8BfL$rw5pWsxJEU4JrZAQhHTCO+u0f5Y='), + ] + +@skipUnless(hash.bcrypt.has_backend(), "no bcrypt backends available") +class django_bcrypt_test(HandlerCase, _DjangoHelper): + """test django_bcrypt""" + handler = hash.django_bcrypt + # XXX: not sure when this wasn't in default list anymore. somewhere in [2.0 - 2.2] + max_django_version = (2, 0) + fuzz_salts_need_bcrypt_repair = True + + known_correct_hashes = [ + # + # just copied and adapted a few test vectors from bcrypt (above), + # since django_bcrypt is just a wrapper for the real bcrypt class. + # + ('', 'bcrypt$$2a$06$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s.'), + ('abcdefghijklmnopqrstuvwxyz', + 'bcrypt$$2a$10$fVH8e28OQRj9tqiDXs1e1uxpsjN0c7II7YPKXua2NAKYvM6iQk7dq'), + (UPASS_TABLE, + 'bcrypt$$2a$05$Z17AXnnlpzddNUvnC6cZNOSwMA/8oNiKnHTHTwLlBijfucQQlHjaG'), + ] + + # NOTE: the following have been cloned from _bcrypt_test() + + def populate_settings(self, kwds): + # speed up test w/ lower rounds + kwds.setdefault("rounds", 4) + super(django_bcrypt_test, self).populate_settings(kwds) + + class FuzzHashGenerator(HandlerCase.FuzzHashGenerator): + + def random_rounds(self): + # decrease default rounds for fuzz testing to speed up volume. + return self.randintgauss(5, 8, 6, 1) + + def random_ident(self): + # omit multi-ident tests, only $2a$ counts for this class + # XXX: enable this to check 2a / 2b? + return None + +@skipUnless(hash.bcrypt.has_backend(), "no bcrypt backends available") +class django_bcrypt_sha256_test(HandlerCase, _DjangoHelper): + """test django_bcrypt_sha256""" + handler = hash.django_bcrypt_sha256 + forbidden_characters = None + fuzz_salts_need_bcrypt_repair = True + + known_correct_hashes = [ + # + # custom - generated via django 1.6 hasher + # + ('', + 'bcrypt_sha256$$2a$06$/3OeRpbOf8/l6nPPRdZPp.nRiyYqPobEZGdNRBWihQhiFDh1ws1tu'), + (UPASS_LETMEIN, + 'bcrypt_sha256$$2a$08$NDjSAIcas.EcoxCRiArvT.MkNiPYVhrsrnJsRkLueZOoV1bsQqlmC'), + (UPASS_TABLE, + 'bcrypt_sha256$$2a$06$kCXUnRFQptGg491siDKNTu8RxjBGSjALHRuvhPYNFsa4Ea5d9M48u'), + + # test >72 chars is hashed correctly -- under bcrypt these hash the same. + (repeat_string("abc123",72), + 'bcrypt_sha256$$2a$06$Tg/oYyZTyAf.Nb3qSgN61OySmyXA8FoY4PjGizjE1QSDfuL5MXNni'), + (repeat_string("abc123",72)+"qwr", + 'bcrypt_sha256$$2a$06$Tg/oYyZTyAf.Nb3qSgN61Ocy0BEz1RK6xslSNi8PlaLX2pe7x/KQG'), + (repeat_string("abc123",72)+"xyz", + 'bcrypt_sha256$$2a$06$Tg/oYyZTyAf.Nb3qSgN61OvY2zoRVUa2Pugv2ExVOUT2YmhvxUFUa'), + ] + + known_malformed_hashers = [ + # data in django salt field + 'bcrypt_sha256$xyz$2a$06$/3OeRpbOf8/l6nPPRdZPp.nRiyYqPobEZGdNRBWihQhiFDh1ws1tu', + ] + + # NOTE: the following have been cloned from _bcrypt_test() + + def populate_settings(self, kwds): + # speed up test w/ lower rounds + kwds.setdefault("rounds", 4) + super(django_bcrypt_sha256_test, self).populate_settings(kwds) + + class FuzzHashGenerator(HandlerCase.FuzzHashGenerator): + + def random_rounds(self): + # decrease default rounds for fuzz testing to speed up volume. + return self.randintgauss(5, 8, 6, 1) + + def random_ident(self): + # omit multi-ident tests, only $2a$ counts for this class + # XXX: enable this to check 2a / 2b? + return None + +from passlib.tests.test_handlers_argon2 import _base_argon2_test + +@skipUnless(hash.argon2.has_backend(), "no argon2 backends available") +class django_argon2_test(HandlerCase, _DjangoHelper): + """test django_bcrypt""" + handler = hash.django_argon2 + + # NOTE: most of this adapted from _base_argon2_test & argon2pure test + + known_correct_hashes = [ + # sample test + ("password", 'argon2$argon2i$v=19$m=256,t=1,p=1$c29tZXNhbHQ$AJFIsNZTMKTAewB4+ETN1A'), + + # sample w/ all parameters different + ("password", 'argon2$argon2i$v=19$m=380,t=2,p=2$c29tZXNhbHQ$SrssP8n7m/12VWPM8dvNrw'), + + # generated from django 1.10.3 + (UPASS_LETMEIN, 'argon2$argon2i$v=19$m=512,t=2,p=2$V25jN1l4UUJZWkR1$MxpA1BD2Gh7+D79gaAw6sQ'), + ] + + def setUpWarnings(self): + super(django_argon2_test, self).setUpWarnings() + warnings.filterwarnings("ignore", ".*Using argon2pure backend.*") + + def do_stub_encrypt(self, handler=None, **settings): + # overriding default since no way to get stub config from argon2._calc_hash() + # (otherwise test_21b_max_rounds blocks trying to do max rounds) + handler = (handler or self.handler).using(**settings) + self = handler.wrapped(use_defaults=True) + self.checksum = self._stub_checksum + assert self.checksum + return handler._wrap_hash(self.to_string()) + + def test_03_legacy_hash_workflow(self): + # override base method + raise self.skipTest("legacy 1.6 workflow not supported") + + class FuzzHashGenerator(_base_argon2_test.FuzzHashGenerator): + + def random_type(self): + # override default since django only uses type I (see note in class) + return "I" + + def random_rounds(self): + # decrease default rounds for fuzz testing to speed up volume. + return self.randintgauss(1, 3, 2, 1) + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/test_handlers_pbkdf2.py b/ansible/lib/python3.11/site-packages/passlib/tests/test_handlers_pbkdf2.py new file mode 100644 index 000000000..4d2f048f0 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/tests/test_handlers_pbkdf2.py @@ -0,0 +1,480 @@ +"""passlib.tests.test_handlers - tests for passlib hash algorithms""" +#============================================================================= +# imports +#============================================================================= +# core +import logging +log = logging.getLogger(__name__) +import warnings +# site +# pkg +from passlib import hash +from passlib.utils.compat import u +from passlib.tests.utils import TestCase, HandlerCase +from passlib.tests.test_handlers import UPASS_WAV +# module + +#============================================================================= +# ldap_pbkdf2_{digest} +#============================================================================= +# NOTE: since these are all wrappers for the pbkdf2_{digest} hasehs, +# they don't extensive separate testing. + +class ldap_pbkdf2_test(TestCase): + + def test_wrappers(self): + """test ldap pbkdf2 wrappers""" + + self.assertTrue( + hash.ldap_pbkdf2_sha1.verify( + "password", + '{PBKDF2}1212$OB.dtnSEXZK8U5cgxU/GYQ$y5LKPOplRmok7CZp/aqVDVg8zGI', + ) + ) + + self.assertTrue( + hash.ldap_pbkdf2_sha256.verify( + "password", + '{PBKDF2-SHA256}1212$4vjV83LKPjQzk31VI4E0Vw$hsYF68OiOUPdDZ1Fg' + '.fJPeq1h/gXXY7acBp9/6c.tmQ' + ) + ) + + self.assertTrue( + hash.ldap_pbkdf2_sha512.verify( + "password", + '{PBKDF2-SHA512}1212$RHY0Fr3IDMSVO/RSZyb5ow$eNLfBK.eVozomMr.1gYa1' + '7k9B7KIK25NOEshvhrSX.esqY3s.FvWZViXz4KoLlQI.BzY/YTNJOiKc5gBYFYGww' + ) + ) + +#============================================================================= +# pbkdf2 hashes +#============================================================================= +class atlassian_pbkdf2_sha1_test(HandlerCase): + handler = hash.atlassian_pbkdf2_sha1 + + known_correct_hashes = [ + # + # generated using Jira + # + ("admin", '{PKCS5S2}c4xaeTQM0lUieMS3V5voiexyX9XhqC2dBd5ecVy60IPksHChwoTAVYFrhsgoq8/p'), + (UPASS_WAV, + "{PKCS5S2}cE9Yq6Am5tQGdHSHhky2XLeOnURwzaLBG2sur7FHKpvy2u0qDn6GcVGRjlmJoIUy"), + ] + + known_malformed_hashes = [ + # bad char ---\/ + '{PKCS5S2}c4xaeTQM0lUieMS3V5voiexyX9XhqC2dBd5ecVy!0IPksHChwoTAVYFrhsgoq8/p' + + # bad size, missing padding + '{PKCS5S2}c4xaeTQM0lUieMS3V5voiexyX9XhqC2dBd5ecVy60IPksHChwoTAVYFrhsgoq8/' + + # bad size, with correct padding + '{PKCS5S2}c4xaeTQM0lUieMS3V5voiexyX9XhqC2dBd5ecVy60IPksHChwoTAVYFrhsgoq8/=' + ] + +class pbkdf2_sha1_test(HandlerCase): + handler = hash.pbkdf2_sha1 + known_correct_hashes = [ + ("password", '$pbkdf2$1212$OB.dtnSEXZK8U5cgxU/GYQ$y5LKPOplRmok7CZp/aqVDVg8zGI'), + (UPASS_WAV, + '$pbkdf2$1212$THDqatpidANpadlLeTeOEg$HV3oi1k5C5LQCgG1BMOL.BX4YZc'), + ] + + known_malformed_hashes = [ + # zero padded rounds field + '$pbkdf2$01212$THDqatpidANpadlLeTeOEg$HV3oi1k5C5LQCgG1BMOL.BX4YZc', + + # empty rounds field + '$pbkdf2$$THDqatpidANpadlLeTeOEg$HV3oi1k5C5LQCgG1BMOL.BX4YZc', + + # too many field + '$pbkdf2$1212$THDqatpidANpadlLeTeOEg$HV3oi1k5C5LQCgG1BMOL.BX4YZc$', + ] + +class pbkdf2_sha256_test(HandlerCase): + handler = hash.pbkdf2_sha256 + known_correct_hashes = [ + ("password", + '$pbkdf2-sha256$1212$4vjV83LKPjQzk31VI4E0Vw$hsYF68OiOUPdDZ1Fg.fJPeq1h/gXXY7acBp9/6c.tmQ' + ), + (UPASS_WAV, + '$pbkdf2-sha256$1212$3SABFJGDtyhrQMVt1uABPw$WyaUoqCLgvz97s523nF4iuOqZNbp5Nt8do/cuaa7AiI' + ), + ] + +class pbkdf2_sha512_test(HandlerCase): + handler = hash.pbkdf2_sha512 + known_correct_hashes = [ + ("password", + '$pbkdf2-sha512$1212$RHY0Fr3IDMSVO/RSZyb5ow$eNLfBK.eVozomMr.1gYa1' + '7k9B7KIK25NOEshvhrSX.esqY3s.FvWZViXz4KoLlQI.BzY/YTNJOiKc5gBYFYGww' + ), + (UPASS_WAV, + '$pbkdf2-sha512$1212$KkbvoKGsAIcF8IslDR6skQ$8be/PRmd88Ps8fmPowCJt' + 'tH9G3vgxpG.Krjt3KT.NP6cKJ0V4Prarqf.HBwz0dCkJ6xgWnSj2ynXSV7MlvMa8Q' + ), + ] + +class cta_pbkdf2_sha1_test(HandlerCase): + handler = hash.cta_pbkdf2_sha1 + known_correct_hashes = [ + # + # test vectors from original implementation + # + (u("hashy the \N{SNOWMAN}"), '$p5k2$1000$ZxK4ZBJCfQg=$jJZVscWtO--p1-xIZl6jhO2LKR0='), + + # + # custom + # + ("password", "$p5k2$1$$h1TDLGSw9ST8UMAPeIE13i0t12c="), + (UPASS_WAV, + "$p5k2$4321$OTg3NjU0MzIx$jINJrSvZ3LXeIbUdrJkRpN62_WQ="), + ] + +class dlitz_pbkdf2_sha1_test(HandlerCase): + handler = hash.dlitz_pbkdf2_sha1 + known_correct_hashes = [ + # + # test vectors from original implementation + # + ('cloadm', '$p5k2$$exec$r1EWMCMk7Rlv3L/RNcFXviDefYa0hlql'), + ('gnu', '$p5k2$c$u9HvcT4d$Sd1gwSVCLZYAuqZ25piRnbBEoAesaa/g'), + ('dcl', '$p5k2$d$tUsch7fU$nqDkaxMDOFBeJsTSfABsyn.PYUXilHwL'), + ('spam', '$p5k2$3e8$H0NX9mT/$wk/sE8vv6OMKuMaqazCJYDSUhWY9YB2J'), + (UPASS_WAV, + '$p5k2$$KosHgqNo$9mjN8gqjt02hDoP0c2J0ABtLIwtot8cQ'), + ] + +class grub_pbkdf2_sha512_test(HandlerCase): + handler = hash.grub_pbkdf2_sha512 + known_correct_hashes = [ + # + # test vectors generated from cmd line tool + # + + # salt=32 bytes + (UPASS_WAV, + 'grub.pbkdf2.sha512.10000.BCAC1CEC5E4341C8C511C529' + '7FA877BE91C2817B32A35A3ECF5CA6B8B257F751.6968526A' + '2A5B1AEEE0A29A9E057336B48D388FFB3F600233237223C21' + '04DE1752CEC35B0DD1ED49563398A282C0F471099C2803FBA' + '47C7919CABC43192C68F60'), + + # salt=64 bytes + ('toomanysecrets', + 'grub.pbkdf2.sha512.10000.9B436BB6978682363D5C449B' + 'BEAB322676946C632208BC1294D51F47174A9A3B04A7E4785' + '986CD4EA7470FAB8FE9F6BD522D1FC6C51109A8596FB7AD48' + '7C4493.0FE5EF169AFFCB67D86E2581B1E251D88C777B98BA' + '2D3256ECC9F765D84956FC5CA5C4B6FD711AA285F0A04DCF4' + '634083F9A20F4B6F339A52FBD6BED618E527B'), + + ] + +#============================================================================= +# scram hash +#============================================================================= +class scram_test(HandlerCase): + handler = hash.scram + + # TODO: need a bunch more reference vectors from some real + # SCRAM transactions. + known_correct_hashes = [ + # + # taken from example in SCRAM specification (rfc 5802) + # + ('pencil', '$scram$4096$QSXCR.Q6sek8bf92$' + 'sha-1=HZbuOlKbWl.eR8AfIposuKbhX30'), + + # + # custom + # + + # same as 5802 example hash, but with sha-256 & sha-512 added. + ('pencil', '$scram$4096$QSXCR.Q6sek8bf92$' + 'sha-1=HZbuOlKbWl.eR8AfIposuKbhX30,' + 'sha-256=qXUXrlcvnaxxWG00DdRgVioR2gnUpuX5r.3EZ1rdhVY,' + 'sha-512=lzgniLFcvglRLS0gt.C4gy.NurS3OIOVRAU1zZOV4P.qFiVFO2/' + 'edGQSu/kD1LwdX0SNV/KsPdHSwEl5qRTuZQ'), + + # test unicode passwords & saslprep (all the passwords below + # should normalize to the same value: 'IX \xE0') + (u('IX \xE0'), '$scram$6400$0BojBCBE6P2/N4bQ$' + 'sha-1=YniLes.b8WFMvBhtSACZyyvxeCc'), + (u('\u2168\u3000a\u0300'), '$scram$6400$0BojBCBE6P2/N4bQ$' + 'sha-1=YniLes.b8WFMvBhtSACZyyvxeCc'), + (u('\u00ADIX \xE0'), '$scram$6400$0BojBCBE6P2/N4bQ$' + 'sha-1=YniLes.b8WFMvBhtSACZyyvxeCc'), + ] + + known_malformed_hashes = [ + # zero-padding in rounds + '$scram$04096$QSXCR.Q6sek8bf92$sha-1=HZbuOlKbWl.eR8AfIposuKbhX30', + + # non-digit in rounds + '$scram$409A$QSXCR.Q6sek8bf92$sha-1=HZbuOlKbWl.eR8AfIposuKbhX30', + + # bad char in salt ---\/ + '$scram$4096$QSXCR.Q6sek8bf9-$sha-1=HZbuOlKbWl.eR8AfIposuKbhX30', + + # bad char in digest ---\/ + '$scram$4096$QSXCR.Q6sek8bf92$sha-1=HZbuOlKbWl.eR8AfIposuKbhX3-', + + # missing sections + '$scram$4096$QSXCR.Q6sek8bf92', + '$scram$4096$QSXCR.Q6sek8bf92$', + + # too many sections + '$scram$4096$QSXCR.Q6sek8bf92$sha-1=HZbuOlKbWl.eR8AfIposuKbhX30$', + + # missing separator + '$scram$4096$QSXCR.Q6sek8bf92$sha-1=HZbuOlKbWl.eR8AfIposuKbhX30' + 'sha-256=qXUXrlcvnaxxWG00DdRgVioR2gnUpuX5r.3EZ1rdhVY', + + # too many chars in alg name + '$scram$4096$QSXCR.Q6sek8bf92$sha-1=HZbuOlKbWl.eR8AfIposuKbhX30,' + 'shaxxx-190=HZbuOlKbWl.eR8AfIposuKbhX30', + + # missing sha-1 alg + '$scram$4096$QSXCR.Q6sek8bf92$sha-256=HZbuOlKbWl.eR8AfIposuKbhX30', + + # non-iana name + '$scram$4096$QSXCR.Q6sek8bf92$sha1=HZbuOlKbWl.eR8AfIposuKbhX30', + ] + + def setUp(self): + super(scram_test, self).setUp() + + # some platforms lack stringprep (e.g. Jython, IronPython) + self.require_stringprep() + + # silence norm_hash_name() warning + warnings.filterwarnings("ignore", r"norm_hash_name\(\): unknown hash") + + def test_90_algs(self): + """test parsing of 'algs' setting""" + defaults = dict(salt=b'A'*10, rounds=1000) + def parse(algs, **kwds): + for k in defaults: + kwds.setdefault(k, defaults[k]) + return self.handler(algs=algs, **kwds).algs + + # None -> default list + self.assertEqual(parse(None, use_defaults=True), hash.scram.default_algs) + self.assertRaises(TypeError, parse, None) + + # strings should be parsed + self.assertEqual(parse("sha1"), ["sha-1"]) + self.assertEqual(parse("sha1, sha256, md5"), ["md5","sha-1","sha-256"]) + + # lists should be normalized + self.assertEqual(parse(["sha-1","sha256"]), ["sha-1","sha-256"]) + + # sha-1 required + self.assertRaises(ValueError, parse, ["sha-256"]) + self.assertRaises(ValueError, parse, algs=[], use_defaults=True) + + # alg names must be < 10 chars + self.assertRaises(ValueError, parse, ["sha-1","shaxxx-190"]) + + # alg & checksum mutually exclusive. + self.assertRaises(RuntimeError, parse, ['sha-1'], + checksum={"sha-1": b"\x00"*20}) + + def test_90_checksums(self): + """test internal parsing of 'checksum' keyword""" + # check non-bytes checksum values are rejected + self.assertRaises(TypeError, self.handler, use_defaults=True, + checksum={'sha-1': u('X')*20}) + + # check sha-1 is required + self.assertRaises(ValueError, self.handler, use_defaults=True, + checksum={'sha-256': b'X'*32}) + + # XXX: anything else that's not tested by the other code already? + + def test_91_extract_digest_info(self): + """test scram.extract_digest_info()""" + edi = self.handler.extract_digest_info + + # return appropriate value or throw KeyError + h = "$scram$10$AAAAAA$sha-1=AQ,bbb=Ag,ccc=Aw" + s = b'\x00'*4 + self.assertEqual(edi(h,"SHA1"), (s,10, b'\x01')) + self.assertEqual(edi(h,"bbb"), (s,10, b'\x02')) + self.assertEqual(edi(h,"ccc"), (s,10, b'\x03')) + self.assertRaises(KeyError, edi, h, "ddd") + + # config strings should cause value error. + c = "$scram$10$....$sha-1,bbb,ccc" + self.assertRaises(ValueError, edi, c, "sha-1") + self.assertRaises(ValueError, edi, c, "bbb") + self.assertRaises(ValueError, edi, c, "ddd") + + def test_92_extract_digest_algs(self): + """test scram.extract_digest_algs()""" + eda = self.handler.extract_digest_algs + + self.assertEqual(eda('$scram$4096$QSXCR.Q6sek8bf92$' + 'sha-1=HZbuOlKbWl.eR8AfIposuKbhX30'), ["sha-1"]) + + self.assertEqual(eda('$scram$4096$QSXCR.Q6sek8bf92$' + 'sha-1=HZbuOlKbWl.eR8AfIposuKbhX30', format="hashlib"), + ["sha1"]) + + self.assertEqual(eda('$scram$4096$QSXCR.Q6sek8bf92$' + 'sha-1=HZbuOlKbWl.eR8AfIposuKbhX30,' + 'sha-256=qXUXrlcvnaxxWG00DdRgVioR2gnUpuX5r.3EZ1rdhVY,' + 'sha-512=lzgniLFcvglRLS0gt.C4gy.NurS3OIOVRAU1zZOV4P.qFiVFO2/' + 'edGQSu/kD1LwdX0SNV/KsPdHSwEl5qRTuZQ'), + ["sha-1","sha-256","sha-512"]) + + def test_93_derive_digest(self): + """test scram.derive_digest()""" + # NOTE: this just does a light test, since derive_digest + # is used by hash / verify, and is tested pretty well via those. + hash = self.handler.derive_digest + + # check various encodings of password work. + s1 = b'\x01\x02\x03' + d1 = b'\xb2\xfb\xab\x82[tNuPnI\x8aZZ\x19\x87\xcen\xe9\xd3' + self.assertEqual(hash(u("\u2168"), s1, 1000, 'sha-1'), d1) + self.assertEqual(hash(b"\xe2\x85\xa8", s1, 1000, 'SHA-1'), d1) + self.assertEqual(hash(u("IX"), s1, 1000, 'sha1'), d1) + self.assertEqual(hash(b"IX", s1, 1000, 'SHA1'), d1) + + # check algs + self.assertEqual(hash("IX", s1, 1000, 'md5'), + b'3\x19\x18\xc0\x1c/\xa8\xbf\xe4\xa3\xc2\x8eM\xe8od') + self.assertRaises(ValueError, hash, "IX", s1, 1000, 'sha-666') + + # check rounds + self.assertRaises(ValueError, hash, "IX", s1, 0, 'sha-1') + + # unicode salts accepted as of passlib 1.7 (previous caused TypeError) + self.assertEqual(hash(u("IX"), s1.decode("latin-1"), 1000, 'sha1'), d1) + + def test_94_saslprep(self): + """test hash/verify use saslprep""" + # NOTE: this just does a light test that saslprep() is being + # called in various places, relying in saslpreps()'s tests + # to verify full normalization behavior. + + # hash unnormalized + h = self.do_encrypt(u("I\u00ADX")) + self.assertTrue(self.do_verify(u("IX"), h)) + self.assertTrue(self.do_verify(u("\u2168"), h)) + + # hash normalized + h = self.do_encrypt(u("\xF3")) + self.assertTrue(self.do_verify(u("o\u0301"), h)) + self.assertTrue(self.do_verify(u("\u200Do\u0301"), h)) + + # throws error if forbidden char provided + self.assertRaises(ValueError, self.do_encrypt, u("\uFDD0")) + self.assertRaises(ValueError, self.do_verify, u("\uFDD0"), h) + + def test_94_using_w_default_algs(self, param="default_algs"): + """using() -- 'default_algs' parameter""" + # create subclass + handler = self.handler + orig = list(handler.default_algs) # in case it's modified in place + subcls = handler.using(**{param: "sha1,md5"}) + + # shouldn't have changed handler + self.assertEqual(handler.default_algs, orig) + + # should have own set + self.assertEqual(subcls.default_algs, ["md5", "sha-1"]) + + # test hash output + h1 = subcls.hash("dummy") + self.assertEqual(handler.extract_digest_algs(h1), ["md5", "sha-1"]) + + def test_94_using_w_algs(self): + """using() -- 'algs' parameter""" + self.test_94_using_w_default_algs(param="algs") + + def test_94_needs_update_algs(self): + """needs_update() -- algs setting""" + handler1 = self.handler.using(algs="sha1,md5") + + # shouldn't need update, has same algs + h1 = handler1.hash("dummy") + self.assertFalse(handler1.needs_update(h1)) + + # *currently* shouldn't need update, has superset of algs required by handler2 + # (may change this policy) + handler2 = handler1.using(algs="sha1") + self.assertFalse(handler2.needs_update(h1)) + + # should need update, doesn't have all algs required by handler3 + handler3 = handler1.using(algs="sha1,sha256") + self.assertTrue(handler3.needs_update(h1)) + + def test_95_context_algs(self): + """test handling of 'algs' in context object""" + handler = self.handler + from passlib.context import CryptContext + c1 = CryptContext(["scram"], scram__algs="sha1,md5") + + h = c1.hash("dummy") + self.assertEqual(handler.extract_digest_algs(h), ["md5", "sha-1"]) + self.assertFalse(c1.needs_update(h)) + + c2 = c1.copy(scram__algs="sha1") + self.assertFalse(c2.needs_update(h)) + + c2 = c1.copy(scram__algs="sha1,sha256") + self.assertTrue(c2.needs_update(h)) + + def test_96_full_verify(self): + """test verify(full=True) flag""" + def vpart(s, h): + return self.handler.verify(s, h) + def vfull(s, h): + return self.handler.verify(s, h, full=True) + + # reference + h = ('$scram$4096$QSXCR.Q6sek8bf92$' + 'sha-1=HZbuOlKbWl.eR8AfIposuKbhX30,' + 'sha-256=qXUXrlcvnaxxWG00DdRgVioR2gnUpuX5r.3EZ1rdhVY,' + 'sha-512=lzgniLFcvglRLS0gt.C4gy.NurS3OIOVRAU1zZOV4P.qFiVFO2/' + 'edGQSu/kD1LwdX0SNV/KsPdHSwEl5qRTuZQ') + self.assertTrue(vfull('pencil', h)) + self.assertFalse(vfull('tape', h)) + + # catch truncated digests. + h = ('$scram$4096$QSXCR.Q6sek8bf92$' + 'sha-1=HZbuOlKbWl.eR8AfIposuKbhX30,' + 'sha-256=qXUXrlcvnaxxWG00DdRgVioR2gnUpuX5r.3EZ1rdhV,' # -1 char + 'sha-512=lzgniLFcvglRLS0gt.C4gy.NurS3OIOVRAU1zZOV4P.qFiVFO2/' + 'edGQSu/kD1LwdX0SNV/KsPdHSwEl5qRTuZQ') + self.assertRaises(ValueError, vfull, 'pencil', h) + + # catch padded digests. + h = ('$scram$4096$QSXCR.Q6sek8bf92$' + 'sha-1=HZbuOlKbWl.eR8AfIposuKbhX30,' + 'sha-256=qXUXrlcvnaxxWG00DdRgVioR2gnUpuX5r.3EZ1rdhVYa,' # +1 char + 'sha-512=lzgniLFcvglRLS0gt.C4gy.NurS3OIOVRAU1zZOV4P.qFiVFO2/' + 'edGQSu/kD1LwdX0SNV/KsPdHSwEl5qRTuZQ') + self.assertRaises(ValueError, vfull, 'pencil', h) + + # catch hash containing digests belonging to diff passwords. + # proper behavior for quick-verify (the default) is undefined, + # but full-verify should throw error. + h = ('$scram$4096$QSXCR.Q6sek8bf92$' + 'sha-1=HZbuOlKbWl.eR8AfIposuKbhX30,' # 'pencil' + 'sha-256=R7RJDWIbeKRTFwhE9oxh04kab0CllrQ3kCcpZUcligc,' # 'tape' + 'sha-512=lzgniLFcvglRLS0gt.C4gy.NurS3OIOVRAU1zZOV4P.qFiVFO2/' # 'pencil' + 'edGQSu/kD1LwdX0SNV/KsPdHSwEl5qRTuZQ') + self.assertTrue(vpart('tape', h)) + self.assertFalse(vpart('pencil', h)) + self.assertRaises(ValueError, vfull, 'pencil', h) + self.assertRaises(ValueError, vfull, 'tape', h) + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/test_handlers_scrypt.py b/ansible/lib/python3.11/site-packages/passlib/tests/test_handlers_scrypt.py new file mode 100644 index 000000000..5ab6d9fb5 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/tests/test_handlers_scrypt.py @@ -0,0 +1,111 @@ +"""passlib.tests.test_handlers - tests for passlib hash algorithms""" +#============================================================================= +# imports +#============================================================================= +# core +import logging; log = logging.getLogger(__name__) +import warnings +warnings.filterwarnings("ignore", ".*using builtin scrypt backend.*") +# site +# pkg +from passlib import hash +from passlib.tests.utils import HandlerCase, TEST_MODE +from passlib.tests.test_handlers import UPASS_TABLE, PASS_TABLE_UTF8 +# module + +#============================================================================= +# scrypt hash +#============================================================================= +class _scrypt_test(HandlerCase): + handler = hash.scrypt + + known_correct_hashes = [ + # + # excepted from test vectors from scrypt whitepaper + # (http://www.tarsnap.com/scrypt/scrypt.pdf, appendix b), + # and encoded using passlib's custom format + # + + # salt=b"" + ("", "$scrypt$ln=4,r=1,p=1$$d9ZXYjhleyA7GcpCwYoEl/FrSETjB0ro39/6P+3iFEI"), + + # salt=b"NaCl" + ("password", "$scrypt$ln=10,r=8,p=16$TmFDbA$/bq+HJ00cgB4VucZDQHp/nxq18vII3gw53N2Y0s3MWI"), + + # + # custom + # + + # simple test + ("test", '$scrypt$ln=8,r=8,p=1$wlhLyXmP8b53bm1NKYVQqg$mTpvG8lzuuDk+DWz8HZIB6Vum6erDuUm0As5yU+VxWA'), + + # different block value + ("password", '$scrypt$ln=8,r=2,p=1$dO6d0xoDoLT2PofQGoNQag$g/Wf2A0vhHhaJM+addK61QPBthSmYB6uVTtQzh8CM3o'), + + # different rounds + (UPASS_TABLE, '$scrypt$ln=7,r=8,p=1$jjGmtDamdA4BQAjBeA9BSA$OiWRHhQtpDx7M/793x6UXK14AD512jg/qNm/hkWZG4M'), + + # alt encoding + (PASS_TABLE_UTF8, '$scrypt$ln=7,r=8,p=1$jjGmtDamdA4BQAjBeA9BSA$OiWRHhQtpDx7M/793x6UXK14AD512jg/qNm/hkWZG4M'), + + # diff block & parallel counts as well + ("nacl", '$scrypt$ln=1,r=4,p=2$yhnD+J+Tci4lZCwFgHCuVQ$fAsEWmxSHuC0cHKMwKVFPzrQukgvK09Sj+NueTSxKds') + ] + + if TEST_MODE("full"): + # add some hashes with larger rounds value. + known_correct_hashes.extend([ + # + # from scrypt whitepaper + # + + # salt=b"SodiumChloride" + ("pleaseletmein", "$scrypt$ln=14,r=8,p=1$U29kaXVtQ2hsb3JpZGU" + "$cCO9yzr9c0hGHAbNgf046/2o+7qQT44+qbVD9lRdofI"), + + # + # openwall format (https://gitlab.com/jas/scrypt-unix-crypt/blob/master/unix-scrypt.txt) + # + ("pleaseletmein", + "$7$C6..../....SodiumChloride$kBGj9fHznVYFQMEn/qDCfrDevf9YDtcDdKvEqHJLV8D"), + + ]) + + known_malformed_hashes = [ + # missing 'p' value + '$scrypt$ln=10,r=1$wvif8/4fg1Cq9V7L2dv73w$bJcLia1lyfQ1X2x0xflehwVXPzWIUQWWdnlGwfVzBeQ', + + # rounds too low + '$scrypt$ln=0,r=1,p=1$wvif8/4fg1Cq9V7L2dv73w$bJcLia1lyfQ1X2x0xflehwVXPzWIUQWWdnlGwfVzBeQ', + + # invalid block size + '$scrypt$ln=10,r=A,p=1$wvif8/4fg1Cq9V7L2dv73w$bJcLia1lyfQ1X2x0xflehwVXPzWIUQWWdnlGwfVzBeQ', + + # r*p too large + '$scrypt$ln=10,r=134217728,p=8$wvif8/4fg1Cq9V7L2dv73w$bJcLia1lyfQ1X2x0xflehwVXPzWIUQWWdnlGwfVzBeQ', + ] + + def setUpWarnings(self): + super(_scrypt_test, self).setUpWarnings() + warnings.filterwarnings("ignore", ".*using builtin scrypt backend.*") + + def populate_settings(self, kwds): + # builtin is still just way too slow. + if self.backend == "builtin": + kwds.setdefault("rounds", 6) + super(_scrypt_test, self).populate_settings(kwds) + + class FuzzHashGenerator(HandlerCase.FuzzHashGenerator): + + def random_rounds(self): + # decrease default rounds for fuzz testing to speed up volume. + return self.randintgauss(4, 10, 6, 1) + +# create test cases for specific backends +scrypt_stdlib_test = _scrypt_test.create_backend_case("stdlib") +scrypt_scrypt_test = _scrypt_test.create_backend_case("scrypt") +scrypt_builtin_test = _scrypt_test.create_backend_case("builtin") + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/test_hosts.py b/ansible/lib/python3.11/site-packages/passlib/tests/test_hosts.py new file mode 100644 index 000000000..cbf93ab7a --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/tests/test_hosts.py @@ -0,0 +1,97 @@ +"""test passlib.hosts""" +#============================================================================= +# imports +#============================================================================= +from __future__ import with_statement +# core +import logging; log = logging.getLogger(__name__) +# site +# pkg +from passlib import hosts, hash as hashmod +from passlib.utils import unix_crypt_schemes +from passlib.tests.utils import TestCase +# module + +#============================================================================= +# test predefined app contexts +#============================================================================= +class HostsTest(TestCase): + """perform general tests to make sure contexts work""" + # NOTE: these tests are not really comprehensive, + # since they would do little but duplicate + # the presets in apps.py + # + # they mainly try to ensure no typos + # or dynamic behavior foul-ups. + + def check_unix_disabled(self, ctx): + for hash in [ + "", + "!", + "*", + "!$1$TXl/FX/U$BZge.lr.ux6ekjEjxmzwz0", + ]: + self.assertEqual(ctx.identify(hash), 'unix_disabled') + self.assertFalse(ctx.verify('test', hash)) + + def test_linux_context(self): + ctx = hosts.linux_context + for hash in [ + ('$6$rounds=41128$VoQLvDjkaZ6L6BIE$4pt.1Ll1XdDYduEwEYPCMOBiR6W6' + 'znsyUEoNlcVXpv2gKKIbQolgmTGe6uEEVJ7azUxuc8Tf7zV9SD2z7Ij751'), + ('$5$rounds=31817$iZGmlyBQ99JSB5n6$p4E.pdPBWx19OajgjLRiOW0itGny' + 'xDGgMlDcOsfaI17'), + '$1$TXl/FX/U$BZge.lr.ux6ekjEjxmzwz0', + 'kAJJz.Rwp0A/I', + ]: + self.assertTrue(ctx.verify("test", hash)) + self.check_unix_disabled(ctx) + + def test_bsd_contexts(self): + for ctx in [ + hosts.freebsd_context, + hosts.openbsd_context, + hosts.netbsd_context, + ]: + for hash in [ + '$1$TXl/FX/U$BZge.lr.ux6ekjEjxmzwz0', + 'kAJJz.Rwp0A/I', + ]: + self.assertTrue(ctx.verify("test", hash)) + h1 = "$2a$04$yjDgE74RJkeqC0/1NheSSOrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIS" + if hashmod.bcrypt.has_backend(): + self.assertTrue(ctx.verify("test", h1)) + else: + self.assertEqual(ctx.identify(h1), "bcrypt") + self.check_unix_disabled(ctx) + + def test_host_context(self): + ctx = getattr(hosts, "host_context", None) + if not ctx: + return self.skipTest("host_context not available on this platform") + + # validate schemes is non-empty, + # and contains unix_disabled + at least one real scheme + schemes = list(ctx.schemes()) + self.assertTrue(schemes, "appears to be unix system, but no known schemes supported by crypt") + self.assertTrue('unix_disabled' in schemes) + schemes.remove("unix_disabled") + self.assertTrue(schemes, "should have schemes beside fallback scheme") + self.assertTrue(set(unix_crypt_schemes).issuperset(schemes)) + + # check for hash support + self.check_unix_disabled(ctx) + for scheme, hash in [ + ("sha512_crypt", ('$6$rounds=41128$VoQLvDjkaZ6L6BIE$4pt.1Ll1XdDYduEwEYPCMOBiR6W6' + 'znsyUEoNlcVXpv2gKKIbQolgmTGe6uEEVJ7azUxuc8Tf7zV9SD2z7Ij751')), + ("sha256_crypt", ('$5$rounds=31817$iZGmlyBQ99JSB5n6$p4E.pdPBWx19OajgjLRiOW0itGny' + 'xDGgMlDcOsfaI17')), + ("md5_crypt", '$1$TXl/FX/U$BZge.lr.ux6ekjEjxmzwz0'), + ("des_crypt", 'kAJJz.Rwp0A/I'), + ]: + if scheme in schemes: + self.assertTrue(ctx.verify("test", hash)) + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/test_pwd.py b/ansible/lib/python3.11/site-packages/passlib/tests/test_pwd.py new file mode 100644 index 000000000..2c983cdf5 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/tests/test_pwd.py @@ -0,0 +1,205 @@ +"""passlib.tests -- tests for passlib.pwd""" +#============================================================================= +# imports +#============================================================================= +# core +import itertools +import logging; log = logging.getLogger(__name__) +# site +# pkg +from passlib.tests.utils import TestCase +# local +__all__ = [ + "UtilsTest", + "GenerateTest", + "StrengthTest", +] + +#============================================================================= +# +#============================================================================= +class UtilsTest(TestCase): + """test internal utilities""" + descriptionPrefix = "passlib.pwd" + + def test_self_info_rate(self): + """_self_info_rate()""" + from passlib.pwd import _self_info_rate + + self.assertEqual(_self_info_rate(""), 0) + + self.assertEqual(_self_info_rate("a" * 8), 0) + + self.assertEqual(_self_info_rate("ab"), 1) + self.assertEqual(_self_info_rate("ab" * 8), 1) + + self.assertEqual(_self_info_rate("abcd"), 2) + self.assertEqual(_self_info_rate("abcd" * 8), 2) + self.assertAlmostEqual(_self_info_rate("abcdaaaa"), 1.5488, places=4) + + # def test_total_self_info(self): + # """_total_self_info()""" + # from passlib.pwd import _total_self_info + # + # self.assertEqual(_total_self_info(""), 0) + # + # self.assertEqual(_total_self_info("a" * 8), 0) + # + # self.assertEqual(_total_self_info("ab"), 2) + # self.assertEqual(_total_self_info("ab" * 8), 16) + # + # self.assertEqual(_total_self_info("abcd"), 8) + # self.assertEqual(_total_self_info("abcd" * 8), 64) + # self.assertAlmostEqual(_total_self_info("abcdaaaa"), 12.3904, places=4) + +#============================================================================= +# word generation +#============================================================================= + +# import subject +from passlib.pwd import genword, default_charsets +ascii_62 = default_charsets['ascii_62'] +hex = default_charsets['hex'] + +class WordGeneratorTest(TestCase): + """test generation routines""" + descriptionPrefix = "passlib.pwd.genword()" + + def setUp(self): + super(WordGeneratorTest, self).setUp() + + # patch some RNG references so they're reproducible. + from passlib.pwd import SequenceGenerator + self.patchAttr(SequenceGenerator, "rng", + self.getRandom("pwd generator")) + + def assertResultContents(self, results, count, chars, unique=True): + """check result list matches expected count & charset""" + self.assertEqual(len(results), count) + if unique: + if unique is True: + unique = count + self.assertEqual(len(set(results)), unique) + self.assertEqual(set("".join(results)), set(chars)) + + def test_general(self): + """general behavior""" + + # basic usage + result = genword() + self.assertEqual(len(result), 9) + + # malformed keyword should have useful error. + self.assertRaisesRegex(TypeError, "(?i)unexpected keyword.*badkwd", genword, badkwd=True) + + def test_returns(self): + """'returns' keyword""" + # returns=int option + results = genword(returns=5000) + self.assertResultContents(results, 5000, ascii_62) + + # returns=iter option + gen = genword(returns=iter) + results = [next(gen) for _ in range(5000)] + self.assertResultContents(results, 5000, ascii_62) + + # invalid returns option + self.assertRaises(TypeError, genword, returns='invalid-type') + + def test_charset(self): + """'charset' & 'chars' options""" + # charset option + results = genword(charset="hex", returns=5000) + self.assertResultContents(results, 5000, hex) + + # chars option + # there are 3**3=27 possible combinations + results = genword(length=3, chars="abc", returns=5000) + self.assertResultContents(results, 5000, "abc", unique=27) + + # chars + charset + self.assertRaises(TypeError, genword, chars='abc', charset='hex') + + # TODO: test rng option + +#============================================================================= +# phrase generation +#============================================================================= + +# import subject +from passlib.pwd import genphrase +simple_words = ["alpha", "beta", "gamma"] + +class PhraseGeneratorTest(TestCase): + """test generation routines""" + descriptionPrefix = "passlib.pwd.genphrase()" + + def assertResultContents(self, results, count, words, unique=True, sep=" "): + """check result list matches expected count & charset""" + self.assertEqual(len(results), count) + if unique: + if unique is True: + unique = count + self.assertEqual(len(set(results)), unique) + out = set(itertools.chain.from_iterable(elem.split(sep) for elem in results)) + self.assertEqual(out, set(words)) + + def test_general(self): + """general behavior""" + + # basic usage + result = genphrase() + self.assertEqual(len(result.split(" ")), 4) # 48 / log(7776, 2) ~= 3.7 -> 4 + + # malformed keyword should have useful error. + self.assertRaisesRegex(TypeError, "(?i)unexpected keyword.*badkwd", genphrase, badkwd=True) + + def test_entropy(self): + """'length' & 'entropy' keywords""" + + # custom entropy + result = genphrase(entropy=70) + self.assertEqual(len(result.split(" ")), 6) # 70 / log(7776, 2) ~= 5.4 -> 6 + + # custom length + result = genphrase(length=3) + self.assertEqual(len(result.split(" ")), 3) + + # custom length < entropy + result = genphrase(length=3, entropy=48) + self.assertEqual(len(result.split(" ")), 4) + + # custom length > entropy + result = genphrase(length=4, entropy=12) + self.assertEqual(len(result.split(" ")), 4) + + def test_returns(self): + """'returns' keyword""" + # returns=int option + results = genphrase(returns=1000, words=simple_words) + self.assertResultContents(results, 1000, simple_words) + + # returns=iter option + gen = genphrase(returns=iter, words=simple_words) + results = [next(gen) for _ in range(1000)] + self.assertResultContents(results, 1000, simple_words) + + # invalid returns option + self.assertRaises(TypeError, genphrase, returns='invalid-type') + + def test_wordset(self): + """'wordset' & 'words' options""" + # wordset option + results = genphrase(words=simple_words, returns=5000) + self.assertResultContents(results, 5000, simple_words) + + # words option + results = genphrase(length=3, words=simple_words, returns=5000) + self.assertResultContents(results, 5000, simple_words, unique=3**3) + + # words + wordset + self.assertRaises(TypeError, genphrase, words=simple_words, wordset='bip39') + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/test_registry.py b/ansible/lib/python3.11/site-packages/passlib/tests/test_registry.py new file mode 100644 index 000000000..8cec48df0 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/tests/test_registry.py @@ -0,0 +1,228 @@ +"""tests for passlib.hash -- (c) Assurance Technologies 2003-2009""" +#============================================================================= +# imports +#============================================================================= +from __future__ import with_statement +# core +from logging import getLogger +import warnings +import sys +# site +# pkg +from passlib import hash, registry, exc +from passlib.registry import register_crypt_handler, register_crypt_handler_path, \ + get_crypt_handler, list_crypt_handlers, _unload_handler_name as unload_handler_name +import passlib.utils.handlers as uh +from passlib.tests.utils import TestCase +# module +log = getLogger(__name__) + +#============================================================================= +# dummy handlers +# +# NOTE: these are defined outside of test case +# since they're used by test_register_crypt_handler_path(), +# which needs them to be available as module globals. +#============================================================================= +class dummy_0(uh.StaticHandler): + name = "dummy_0" + +class alt_dummy_0(uh.StaticHandler): + name = "dummy_0" + +dummy_x = 1 + +#============================================================================= +# test registry +#============================================================================= +class RegistryTest(TestCase): + + descriptionPrefix = "passlib.registry" + + def setUp(self): + super(RegistryTest, self).setUp() + + # backup registry state & restore it after test. + locations = dict(registry._locations) + handlers = dict(registry._handlers) + def restore(): + registry._locations.clear() + registry._locations.update(locations) + registry._handlers.clear() + registry._handlers.update(handlers) + self.addCleanup(restore) + + def test_hash_proxy(self): + """test passlib.hash proxy object""" + # check dir works + dir(hash) + + # check repr works + repr(hash) + + # check non-existent attrs raise error + self.assertRaises(AttributeError, getattr, hash, 'fooey') + + # GAE tries to set __loader__, + # make sure that doesn't call register_crypt_handler. + old = getattr(hash, "__loader__", None) + test = object() + hash.__loader__ = test + self.assertIs(hash.__loader__, test) + if old is None: + del hash.__loader__ + self.assertFalse(hasattr(hash, "__loader__")) + else: + hash.__loader__ = old + self.assertIs(hash.__loader__, old) + + # check storing attr calls register_crypt_handler + class dummy_1(uh.StaticHandler): + name = "dummy_1" + hash.dummy_1 = dummy_1 + self.assertIs(get_crypt_handler("dummy_1"), dummy_1) + + # check storing under wrong name results in error + self.assertRaises(ValueError, setattr, hash, "dummy_1x", dummy_1) + + def test_register_crypt_handler_path(self): + """test register_crypt_handler_path()""" + # NOTE: this messes w/ internals of registry, shouldn't be used publically. + paths = registry._locations + + # check namespace is clear + self.assertTrue('dummy_0' not in paths) + self.assertFalse(hasattr(hash, 'dummy_0')) + + # check invalid names are rejected + self.assertRaises(ValueError, register_crypt_handler_path, + "dummy_0", ".test_registry") + self.assertRaises(ValueError, register_crypt_handler_path, + "dummy_0", __name__ + ":dummy_0:xxx") + self.assertRaises(ValueError, register_crypt_handler_path, + "dummy_0", __name__ + ":dummy_0.xxx") + + # try lazy load + register_crypt_handler_path('dummy_0', __name__) + self.assertTrue('dummy_0' in list_crypt_handlers()) + self.assertTrue('dummy_0' not in list_crypt_handlers(loaded_only=True)) + self.assertIs(hash.dummy_0, dummy_0) + self.assertTrue('dummy_0' in list_crypt_handlers(loaded_only=True)) + unload_handler_name('dummy_0') + + # try lazy load w/ alt + register_crypt_handler_path('dummy_0', __name__ + ':alt_dummy_0') + self.assertIs(hash.dummy_0, alt_dummy_0) + unload_handler_name('dummy_0') + + # check lazy load w/ wrong type fails + register_crypt_handler_path('dummy_x', __name__) + self.assertRaises(TypeError, get_crypt_handler, 'dummy_x') + + # check lazy load w/ wrong name fails + register_crypt_handler_path('alt_dummy_0', __name__) + self.assertRaises(ValueError, get_crypt_handler, "alt_dummy_0") + unload_handler_name("alt_dummy_0") + + # TODO: check lazy load which calls register_crypt_handler (warning should be issued) + sys.modules.pop("passlib.tests._test_bad_register", None) + register_crypt_handler_path("dummy_bad", "passlib.tests._test_bad_register") + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", "xxxxxxxxxx", DeprecationWarning) + h = get_crypt_handler("dummy_bad") + from passlib.tests import _test_bad_register as tbr + self.assertIs(h, tbr.alt_dummy_bad) + + def test_register_crypt_handler(self): + """test register_crypt_handler()""" + + self.assertRaises(TypeError, register_crypt_handler, {}) + + self.assertRaises(ValueError, register_crypt_handler, type('x', (uh.StaticHandler,), dict(name=None))) + self.assertRaises(ValueError, register_crypt_handler, type('x', (uh.StaticHandler,), dict(name="AB_CD"))) + self.assertRaises(ValueError, register_crypt_handler, type('x', (uh.StaticHandler,), dict(name="ab-cd"))) + self.assertRaises(ValueError, register_crypt_handler, type('x', (uh.StaticHandler,), dict(name="ab__cd"))) + self.assertRaises(ValueError, register_crypt_handler, type('x', (uh.StaticHandler,), dict(name="default"))) + + class dummy_1(uh.StaticHandler): + name = "dummy_1" + + class dummy_1b(uh.StaticHandler): + name = "dummy_1" + + self.assertTrue('dummy_1' not in list_crypt_handlers()) + + register_crypt_handler(dummy_1) + register_crypt_handler(dummy_1) + self.assertIs(get_crypt_handler("dummy_1"), dummy_1) + + self.assertRaises(KeyError, register_crypt_handler, dummy_1b) + self.assertIs(get_crypt_handler("dummy_1"), dummy_1) + + register_crypt_handler(dummy_1b, force=True) + self.assertIs(get_crypt_handler("dummy_1"), dummy_1b) + + self.assertTrue('dummy_1' in list_crypt_handlers()) + + def test_get_crypt_handler(self): + """test get_crypt_handler()""" + + class dummy_1(uh.StaticHandler): + name = "dummy_1" + + # without available handler + self.assertRaises(KeyError, get_crypt_handler, "dummy_1") + self.assertIs(get_crypt_handler("dummy_1", None), None) + + # already loaded handler + register_crypt_handler(dummy_1) + self.assertIs(get_crypt_handler("dummy_1"), dummy_1) + + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", "handler names should be lower-case, and use underscores instead of hyphens:.*", UserWarning) + + # already loaded handler, using incorrect name + self.assertIs(get_crypt_handler("DUMMY-1"), dummy_1) + + # lazy load of unloaded handler, using incorrect name + register_crypt_handler_path('dummy_0', __name__) + self.assertIs(get_crypt_handler("DUMMY-0"), dummy_0) + + # check system & private names aren't returned + from passlib import hash + hash.__dict__["_fake"] = "dummy" + for name in ["_fake", "__package__"]: + self.assertRaises(KeyError, get_crypt_handler, name) + self.assertIs(get_crypt_handler(name, None), None) + + def test_list_crypt_handlers(self): + """test list_crypt_handlers()""" + from passlib.registry import list_crypt_handlers + + # check system & private names aren't returned + hash.__dict__["_fake"] = "dummy" + for name in list_crypt_handlers(): + self.assertFalse(name.startswith("_"), "%r: " % name) + unload_handler_name("_fake") + + def test_handlers(self): + """verify we have tests for all builtin handlers""" + from passlib.registry import list_crypt_handlers + from passlib.tests.test_handlers import get_handler_case, conditionally_available_hashes + for name in list_crypt_handlers(): + # skip some wrappers that don't need independant testing + if name.startswith("ldap_") and name[5:] in list_crypt_handlers(): + continue + if name in ["roundup_plaintext"]: + continue + # check the remaining ones all have a handler + try: + self.assertTrue(get_handler_case(name)) + except exc.MissingBackendError: + if name in conditionally_available_hashes: # expected to fail on some setups + continue + raise + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/test_totp.py b/ansible/lib/python3.11/site-packages/passlib/tests/test_totp.py new file mode 100644 index 000000000..604d2e98a --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/tests/test_totp.py @@ -0,0 +1,1604 @@ +"""passlib.tests -- test passlib.totp""" +#============================================================================= +# imports +#============================================================================= +# core +import datetime +from functools import partial +import logging; log = logging.getLogger(__name__) +import sys +import time as _time +# site +# pkg +from passlib import exc +from passlib.utils.compat import unicode, u +from passlib.tests.utils import TestCase, time_call +# subject +from passlib import totp as totp_module +from passlib.totp import TOTP, AppWallet, AES_SUPPORT +# local +__all__ = [ + "EngineTest", +] + +#============================================================================= +# helpers +#============================================================================= + +# XXX: python 3 changed what error base64.b16decode() throws, from TypeError to base64.Error(). +# it wasn't until 3.3 that base32decode() also got changed. +# really should normalize this in the code to a single BinaryDecodeError, +# predicting this cross-version is getting unmanagable. +Base32DecodeError = Base16DecodeError = TypeError +if sys.version_info >= (3,0): + from binascii import Error as Base16DecodeError +if sys.version_info >= (3,3): + from binascii import Error as Base32DecodeError + +PASS1 = "abcdef" +PASS2 = b"\x00\xFF" +KEY1 = '4AOGGDBBQSYHNTUZ' +KEY1_RAW = b'\xe0\x1cc\x0c!\x84\xb0v\xce\x99' +KEY2_RAW = b'\xee]\xcb9\x870\x06 D\xc8y/\xa54&\xe4\x9c\x13\xc2\x18' +KEY3 = 'S3JDVB7QD2R7JPXX' # used in docstrings +KEY4 = 'JBSWY3DPEHPK3PXP' # from google keyuri spec +KEY4_RAW = b'Hello!\xde\xad\xbe\xef' + +# NOTE: for randtime() below, +# * want at least 7 bits on fractional side, to test fractional times to at least 0.01s precision +# * want at least 32 bits on integer side, to test for 32-bit epoch issues. +# most systems *should* have 53 bit mantissa, leaving plenty of room on both ends, +# so using (1<<37) as scale, to allocate 16 bits on fractional side, but generate reasonable # of > 1<<32 times. +# sanity check that we're above 44 ensures minimum requirements (44 - 37 int = 7 frac) +assert sys.float_info.radix == 2, "unexpected float_info.radix" +assert sys.float_info.mant_dig >= 44, "double precision unexpectedly small" + +def _get_max_time_t(): + """ + helper to calc max_time_t constant (see below) + """ + value = 1 << 30 # even for 32 bit systems will handle this + year = 0 + while True: + next_value = value << 1 + try: + next_year = datetime.datetime.utcfromtimestamp(next_value-1).year + except (ValueError, OSError, OverflowError): + # utcfromtimestamp() may throw any of the following: + # + # * year out of range for datetime: + # py < 3.6 throws ValueError. + # (py 3.6.0 returns odd value instead, see workaround below) + # + # * int out of range for host's gmtime/localtime: + # py2 throws ValueError, py3 throws OSError. + # + # * int out of range for host's time_t: + # py2 throws ValueError, py3 throws OverflowError. + # + break + + # Workaround for python 3.6.0 issue -- + # Instead of throwing ValueError if year out of range for datetime, + # Python 3.6 will do some weird behavior that masks high bits + # e.g. (1<<40) -> year 36812, but (1<<41) -> year 6118. + # (Appears to be bug http://bugs.python.org/issue29100) + # This check stops at largest non-wrapping bit size. + if next_year < year: + break + + value = next_value + + # 'value-1' is maximum. + value -= 1 + + # check for crazy case where we're beyond what datetime supports + # (caused by bug 29100 again). compare to max value that datetime + # module supports -- datetime.datetime(9999, 12, 31, 23, 59, 59, 999999) + max_datetime_timestamp = 253402318800 + return min(value, max_datetime_timestamp) + +#: Rough approximation of max value acceptable by hosts's time_t. +#: This is frequently ~2**37 on 64 bit, and ~2**31 on 32 bit systems. +max_time_t = _get_max_time_t() + +def to_b32_size(raw_size): + return (raw_size * 8 + 4) // 5 + +#============================================================================= +# wallet +#============================================================================= +class AppWalletTest(TestCase): + descriptionPrefix = "passlib.totp.AppWallet" + + #============================================================================= + # constructor + #============================================================================= + + def test_secrets_types(self): + """constructor -- 'secrets' param -- input types""" + + # no secrets + wallet = AppWallet() + self.assertEqual(wallet._secrets, {}) + self.assertFalse(wallet.has_secrets) + + # dict + ref = {"1": b"aaa", "2": b"bbb"} + wallet = AppWallet(ref) + self.assertEqual(wallet._secrets, ref) + self.assertTrue(wallet.has_secrets) + + # # list + # wallet = AppWallet(list(ref.items())) + # self.assertEqual(wallet._secrets, ref) + + # # iter + # wallet = AppWallet(iter(ref.items())) + # self.assertEqual(wallet._secrets, ref) + + # "tag:value" string + wallet = AppWallet("\n 1: aaa\n# comment\n \n2: bbb ") + self.assertEqual(wallet._secrets, ref) + + # ensure ":" allowed in secret + wallet = AppWallet("1: aaa: bbb \n# comment\n \n2: bbb ") + self.assertEqual(wallet._secrets, {"1": b"aaa: bbb", "2": b"bbb"}) + + # json dict + wallet = AppWallet('{"1":"aaa","2":"bbb"}') + self.assertEqual(wallet._secrets, ref) + + # # json list + # wallet = AppWallet('[["1","aaa"],["2","bbb"]]') + # self.assertEqual(wallet._secrets, ref) + + # invalid type + self.assertRaises(TypeError, AppWallet, 123) + + # invalid json obj + self.assertRaises(TypeError, AppWallet, "[123]") + + # # invalid list items + # self.assertRaises(ValueError, AppWallet, ["1", b"aaa"]) + + # forbid empty secret + self.assertRaises(ValueError, AppWallet, {"1": "aaa", "2": ""}) + + def test_secrets_tags(self): + """constructor -- 'secrets' param -- tag/value normalization""" + + # test reference + ref = {"1": b"aaa", "02": b"bbb", "C": b"ccc"} + wallet = AppWallet(ref) + self.assertEqual(wallet._secrets, ref) + + # accept unicode + wallet = AppWallet({u("1"): b"aaa", u("02"): b"bbb", u("C"): b"ccc"}) + self.assertEqual(wallet._secrets, ref) + + # normalize int tags + wallet = AppWallet({1: b"aaa", "02": b"bbb", "C": b"ccc"}) + self.assertEqual(wallet._secrets, ref) + + # forbid non-str/int tags + self.assertRaises(TypeError, AppWallet, {(1,): "aaa"}) + + # accept valid tags + wallet = AppWallet({"1-2_3.4": b"aaa"}) + + # forbid invalid tags + self.assertRaises(ValueError, AppWallet, {"-abc": "aaa"}) + self.assertRaises(ValueError, AppWallet, {"ab*$": "aaa"}) + + # coerce value to bytes + wallet = AppWallet({"1": u("aaa"), "02": "bbb", "C": b"ccc"}) + self.assertEqual(wallet._secrets, ref) + + # forbid invalid value types + self.assertRaises(TypeError, AppWallet, {"1": 123}) + self.assertRaises(TypeError, AppWallet, {"1": None}) + self.assertRaises(TypeError, AppWallet, {"1": []}) + + # TODO: test secrets_path + + def test_default_tag(self): + """constructor -- 'default_tag' param""" + + # should sort numerically + wallet = AppWallet({"1": "one", "02": "two"}) + self.assertEqual(wallet.default_tag, "02") + self.assertEqual(wallet.get_secret(wallet.default_tag), b"two") + + # should sort alphabetically if non-digit present + wallet = AppWallet({"1": "one", "02": "two", "A": "aaa"}) + self.assertEqual(wallet.default_tag, "A") + self.assertEqual(wallet.get_secret(wallet.default_tag), b"aaa") + + # should use honor custom tag + wallet = AppWallet({"1": "one", "02": "two", "A": "aaa"}, default_tag="1") + self.assertEqual(wallet.default_tag, "1") + self.assertEqual(wallet.get_secret(wallet.default_tag), b"one") + + # throw error on unknown value + self.assertRaises(KeyError, AppWallet, {"1": "one", "02": "two", "A": "aaa"}, + default_tag="B") + + # should be empty + wallet = AppWallet() + self.assertEqual(wallet.default_tag, None) + self.assertRaises(KeyError, wallet.get_secret, None) + + # TODO: test 'cost' param + + #============================================================================= + # encrypt_key() & decrypt_key() helpers + #============================================================================= + def require_aes_support(self, canary=None): + if AES_SUPPORT: + canary and canary() + else: + canary and self.assertRaises(RuntimeError, canary) + raise self.skipTest("'cryptography' package not installed") + + def test_decrypt_key(self): + """.decrypt_key()""" + + wallet = AppWallet({"1": PASS1, "2": PASS2}) + + # check for support + CIPHER1 = dict(v=1, c=13, s='6D7N7W53O7HHS37NLUFQ', + k='MHCTEGSNPFN5CGBJ', t='1') + self.require_aes_support(canary=partial(wallet.decrypt_key, CIPHER1)) + + # reference key + self.assertEqual(wallet.decrypt_key(CIPHER1)[0], KEY1_RAW) + + # different salt used to encrypt same raw key + CIPHER2 = dict(v=1, c=13, s='SPZJ54Y6IPUD2BYA4C6A', + k='ZGDXXTVQOWYLC2AU', t='1') + self.assertEqual(wallet.decrypt_key(CIPHER2)[0], KEY1_RAW) + + # different sized key, password, and cost + CIPHER3 = dict(v=1, c=8, s='FCCTARTIJWE7CPQHUDKA', + k='D2DRS32YESGHHINWFFCELKN7Z6NAHM4M', t='2') + self.assertEqual(wallet.decrypt_key(CIPHER3)[0], KEY2_RAW) + + # wrong password should silently result in wrong key + temp = CIPHER1.copy() + temp.update(t='2') + self.assertEqual(wallet.decrypt_key(temp)[0], b'\xafD6.F7\xeb\x19\x05Q') + + # missing tag should throw error + temp = CIPHER1.copy() + temp.update(t='3') + self.assertRaises(KeyError, wallet.decrypt_key, temp) + + # unknown version should throw error + temp = CIPHER1.copy() + temp.update(v=999) + self.assertRaises(ValueError, wallet.decrypt_key, temp) + + def test_decrypt_key_needs_recrypt(self): + """.decrypt_key() -- needs_recrypt flag""" + self.require_aes_support() + + wallet = AppWallet({"1": PASS1, "2": PASS2}, encrypt_cost=13) + + # ref should be accepted + ref = dict(v=1, c=13, s='AAAA', k='AAAA', t='2') + self.assertFalse(wallet.decrypt_key(ref)[1]) + + # wrong cost + temp = ref.copy() + temp.update(c=8) + self.assertTrue(wallet.decrypt_key(temp)[1]) + + # wrong tag + temp = ref.copy() + temp.update(t="1") + self.assertTrue(wallet.decrypt_key(temp)[1]) + + # XXX: should this check salt_size? + + def assertSaneResult(self, result, wallet, key, tag="1", + needs_recrypt=False): + """check encrypt_key() result has expected format""" + + self.assertEqual(set(result), set(["v", "t", "c", "s", "k"])) + + self.assertEqual(result['v'], 1) + self.assertEqual(result['t'], tag) + self.assertEqual(result['c'], wallet.encrypt_cost) + + self.assertEqual(len(result['s']), to_b32_size(wallet.salt_size)) + self.assertEqual(len(result['k']), to_b32_size(len(key))) + + result_key, result_needs_recrypt = wallet.decrypt_key(result) + self.assertEqual(result_key, key) + self.assertEqual(result_needs_recrypt, needs_recrypt) + + def test_encrypt_key(self): + """.encrypt_key()""" + + # check for support + wallet = AppWallet({"1": PASS1}, encrypt_cost=5) + self.require_aes_support(canary=partial(wallet.encrypt_key, KEY1_RAW)) + + # basic behavior + result = wallet.encrypt_key(KEY1_RAW) + self.assertSaneResult(result, wallet, KEY1_RAW) + + # creates new salt each time + other = wallet.encrypt_key(KEY1_RAW) + self.assertSaneResult(result, wallet, KEY1_RAW) + self.assertNotEqual(other['s'], result['s']) + self.assertNotEqual(other['k'], result['k']) + + # honors custom cost + wallet2 = AppWallet({"1": PASS1}, encrypt_cost=6) + result = wallet2.encrypt_key(KEY1_RAW) + self.assertSaneResult(result, wallet2, KEY1_RAW) + + # honors default tag + wallet2 = AppWallet({"1": PASS1, "2": PASS2}) + result = wallet2.encrypt_key(KEY1_RAW) + self.assertSaneResult(result, wallet2, KEY1_RAW, tag="2") + + # honor salt size + wallet2 = AppWallet({"1": PASS1}) + wallet2.salt_size = 64 + result = wallet2.encrypt_key(KEY1_RAW) + self.assertSaneResult(result, wallet2, KEY1_RAW) + + # larger key + result = wallet.encrypt_key(KEY2_RAW) + self.assertSaneResult(result, wallet, KEY2_RAW) + + # border case: empty key + # XXX: might want to allow this, but documenting behavior for now + self.assertRaises(ValueError, wallet.encrypt_key, b"") + + def test_encrypt_cost_timing(self): + """verify cost parameter via timing""" + self.require_aes_support() + + # time default cost + wallet = AppWallet({"1": "aaa"}) + wallet.encrypt_cost -= 2 + delta, _ = time_call(partial(wallet.encrypt_key, KEY1_RAW), maxtime=0) + + # this should take (2**3=8) times as long + wallet.encrypt_cost += 3 + delta2, _ = time_call(partial(wallet.encrypt_key, KEY1_RAW), maxtime=0) + + # TODO: rework timing test here to inject mock pbkdf2_hmac() function instead; + # and test that it's being invoked w/ proper options. + self.assertAlmostEqual(delta2, delta*8, delta=(delta*8)*0.5) + + #============================================================================= + # eoc + #============================================================================= + +#============================================================================= +# common OTP code +#============================================================================= + +#: used as base value for RFC test vector keys +RFC_KEY_BYTES_20 = "12345678901234567890".encode("ascii") +RFC_KEY_BYTES_32 = (RFC_KEY_BYTES_20*2)[:32] +RFC_KEY_BYTES_64 = (RFC_KEY_BYTES_20*4)[:64] + +# TODO: this class is separate from TotpTest due to historical issue, +# when there was a base class, and a separate HOTP class. +# these test case classes should probably be combined. +class TotpTest(TestCase): + """ + common code shared by TotpTest & HotpTest + """ + #============================================================================= + # class attrs + #============================================================================= + + descriptionPrefix = "passlib.totp.TOTP" + + #============================================================================= + # setup + #============================================================================= + def setUp(self): + super(TotpTest, self).setUp() + + # clear norm_hash_name() cache so 'unknown hash' warnings get emitted each time + from passlib.crypto.digest import lookup_hash + lookup_hash.clear_cache() + + # monkeypatch module's rng to be deterministic + self.patchAttr(totp_module, "rng", self.getRandom()) + + #============================================================================= + # general helpers + #============================================================================= + def randtime(self): + """ + helper to generate random epoch time + :returns float: epoch time + """ + return self.getRandom().random() * max_time_t + + def randotp(self, cls=None, **kwds): + """ + helper which generates a random TOTP instance. + """ + rng = self.getRandom() + if "key" not in kwds: + kwds['new'] = True + kwds.setdefault("digits", rng.randint(6, 10)) + kwds.setdefault("alg", rng.choice(["sha1", "sha256", "sha512"])) + kwds.setdefault("period", rng.randint(10, 120)) + return (cls or TOTP)(**kwds) + + def test_randotp(self): + """ + internal test -- randotp() + """ + otp1 = self.randotp() + otp2 = self.randotp() + + self.assertNotEqual(otp1.key, otp2.key, "key not randomized:") + + # NOTE: has (1/5)**10 odds of failure + for _ in range(10): + if otp1.digits != otp2.digits: + break + otp2 = self.randotp() + else: + self.fail("digits not randomized") + + # NOTE: has (1/3)**10 odds of failure + for _ in range(10): + if otp1.alg != otp2.alg: + break + otp2 = self.randotp() + else: + self.fail("alg not randomized") + + #============================================================================= + # reference vector helpers + #============================================================================= + + #: default options used by test vectors (unless otherwise stated) + vector_defaults = dict(format="base32", alg="sha1", period=30, digits=8) + + #: various TOTP test vectors, + #: each element in list has format [options, (time, token <, int(expires)>), ...] + vectors = [ + + #------------------------------------------------------------------------- + # passlib test vectors + #------------------------------------------------------------------------- + + # 10 byte key, 6 digits + [dict(key="ACDEFGHJKL234567", digits=6), + # test fencepost to make sure we're rounding right + (1412873399, '221105'), # == 29 mod 30 + (1412873400, '178491'), # == 0 mod 30 + (1412873401, '178491'), # == 1 mod 30 + (1412873429, '178491'), # == 29 mod 30 + (1412873430, '915114'), # == 0 mod 30 + ], + + # 10 byte key, 8 digits + [dict(key="ACDEFGHJKL234567", digits=8), + # should be same as 6 digits (above), but w/ 2 more digits on left side of token. + (1412873399, '20221105'), # == 29 mod 30 + (1412873400, '86178491'), # == 0 mod 30 + (1412873401, '86178491'), # == 1 mod 30 + (1412873429, '86178491'), # == 29 mod 30 + (1412873430, '03915114'), # == 0 mod 30 + ], + + # sanity check on key used in docstrings + [dict(key="S3JD-VB7Q-D2R7-JPXX", digits=6), + (1419622709, '000492'), + (1419622739, '897212'), + ], + + #------------------------------------------------------------------------- + # reference vectors taken from http://tools.ietf.org/html/rfc6238, appendix B + # NOTE: while appendix B states same key used for all tests, the reference + # code in the appendix repeats the key up to the alg's block size, + # and uses *that* as the secret... so that's what we're doing here. + #------------------------------------------------------------------------- + + # sha1 test vectors + [dict(key=RFC_KEY_BYTES_20, format="raw", alg="sha1"), + (59, '94287082'), + (1111111109, '07081804'), + (1111111111, '14050471'), + (1234567890, '89005924'), + (2000000000, '69279037'), + (20000000000, '65353130'), + ], + + # sha256 test vectors + [dict(key=RFC_KEY_BYTES_32, format="raw", alg="sha256"), + (59, '46119246'), + (1111111109, '68084774'), + (1111111111, '67062674'), + (1234567890, '91819424'), + (2000000000, '90698825'), + (20000000000, '77737706'), + ], + + # sha512 test vectors + [dict(key=RFC_KEY_BYTES_64, format="raw", alg="sha512"), + (59, '90693936'), + (1111111109, '25091201'), + (1111111111, '99943326'), + (1234567890, '93441116'), + (2000000000, '38618901'), + (20000000000, '47863826'), + ], + + #------------------------------------------------------------------------- + # other test vectors + #------------------------------------------------------------------------- + + # generated at http://blog.tinisles.com/2011/10/google-authenticator-one-time-password-algorithm-in-javascript + [dict(key="JBSWY3DPEHPK3PXP", digits=6), (1409192430, '727248'), (1419890990, '122419')], + [dict(key="JBSWY3DPEHPK3PXP", digits=9, period=41), (1419891152, '662331049')], + + # found in https://github.com/eloquent/otis/blob/develop/test/suite/Totp/Value/TotpValueGeneratorTest.php, line 45 + [dict(key=RFC_KEY_BYTES_20, format="raw", period=60), (1111111111, '19360094')], + [dict(key=RFC_KEY_BYTES_32, format="raw", alg="sha256", period=60), (1111111111, '40857319')], + [dict(key=RFC_KEY_BYTES_64, format="raw", alg="sha512", period=60), (1111111111, '37023009')], + + ] + + def iter_test_vectors(self): + """ + helper to iterate over test vectors. + yields ``(totp, time, token, expires, prefix)`` tuples. + """ + from passlib.totp import TOTP + for row in self.vectors: + kwds = self.vector_defaults.copy() + kwds.update(row[0]) + for entry in row[1:]: + if len(entry) == 3: + time, token, expires = entry + else: + time, token = entry + expires = None + # NOTE: not re-using otp between calls so that stateful methods + # (like .match) don't have problems. + log.debug("test vector: %r time=%r token=%r expires=%r", kwds, time, token, expires) + otp = TOTP(**kwds) + prefix = "alg=%r time=%r token=%r: " % (otp.alg, time, token) + yield otp, time, token, expires, prefix + + #============================================================================= + # constructor tests + #============================================================================= + def test_ctor_w_new(self): + """constructor -- 'new' parameter""" + + # exactly one of 'key' or 'new' is required + self.assertRaises(TypeError, TOTP) + self.assertRaises(TypeError, TOTP, key='4aoggdbbqsyhntuz', new=True) + + # generates new key + otp = TOTP(new=True) + otp2 = TOTP(new=True) + self.assertNotEqual(otp.key, otp2.key) + + def test_ctor_w_size(self): + """constructor -- 'size' parameter""" + + # should default to digest size, per RFC + self.assertEqual(len(TOTP(new=True, alg="sha1").key), 20) + self.assertEqual(len(TOTP(new=True, alg="sha256").key), 32) + self.assertEqual(len(TOTP(new=True, alg="sha512").key), 64) + + # explicit key size + self.assertEqual(len(TOTP(new=True, size=10).key), 10) + self.assertEqual(len(TOTP(new=True, size=16).key), 16) + + # for new=True, maximum size enforced (based on alg) + self.assertRaises(ValueError, TOTP, new=True, size=21, alg="sha1") + + # for new=True, minimum size enforced + self.assertRaises(ValueError, TOTP, new=True, size=9) + + # for existing key, minimum size is only warned about + with self.assertWarningList([ + dict(category=exc.PasslibSecurityWarning, message_re=".*for security purposes, secret key must be.*") + ]): + _ = TOTP('0A'*9, 'hex') + + def test_ctor_w_key_and_format(self): + """constructor -- 'key' and 'format' parameters""" + + # handle base32 encoding (the default) + self.assertEqual(TOTP(KEY1).key, KEY1_RAW) + + # .. w/ lower case + self.assertEqual(TOTP(KEY1.lower()).key, KEY1_RAW) + + # .. w/ spaces (e.g. user-entered data) + self.assertEqual(TOTP(' 4aog gdbb qsyh ntuz ').key, KEY1_RAW) + + # .. w/ invalid char + self.assertRaises(Base32DecodeError, TOTP, 'ao!ggdbbqsyhntuz') + + # handle hex encoding + self.assertEqual(TOTP('e01c630c2184b076ce99', 'hex').key, KEY1_RAW) + + # .. w/ invalid char + self.assertRaises(Base16DecodeError, TOTP, 'X01c630c2184b076ce99', 'hex') + + # handle raw bytes + self.assertEqual(TOTP(KEY1_RAW, "raw").key, KEY1_RAW) + + def test_ctor_w_alg(self): + """constructor -- 'alg' parameter""" + + # normalize hash names + self.assertEqual(TOTP(KEY1, alg="SHA-256").alg, "sha256") + self.assertEqual(TOTP(KEY1, alg="SHA256").alg, "sha256") + + # invalid alg + self.assertRaises(ValueError, TOTP, KEY1, alg="SHA-333") + + def test_ctor_w_digits(self): + """constructor -- 'digits' parameter""" + self.assertRaises(ValueError, TOTP, KEY1, digits=5) + self.assertEqual(TOTP(KEY1, digits=6).digits, 6) # min value + self.assertEqual(TOTP(KEY1, digits=10).digits, 10) # max value + self.assertRaises(ValueError, TOTP, KEY1, digits=11) + + def test_ctor_w_period(self): + """constructor -- 'period' parameter""" + + # default + self.assertEqual(TOTP(KEY1).period, 30) + + # explicit value + self.assertEqual(TOTP(KEY1, period=63).period, 63) + + # reject wrong type + self.assertRaises(TypeError, TOTP, KEY1, period=1.5) + self.assertRaises(TypeError, TOTP, KEY1, period='abc') + + # reject non-positive values + self.assertRaises(ValueError, TOTP, KEY1, period=0) + self.assertRaises(ValueError, TOTP, KEY1, period=-1) + + def test_ctor_w_label(self): + """constructor -- 'label' parameter""" + self.assertEqual(TOTP(KEY1).label, None) + self.assertEqual(TOTP(KEY1, label="foo@bar").label, "foo@bar") + self.assertRaises(ValueError, TOTP, KEY1, label="foo:bar") + + def test_ctor_w_issuer(self): + """constructor -- 'issuer' parameter""" + self.assertEqual(TOTP(KEY1).issuer, None) + self.assertEqual(TOTP(KEY1, issuer="foo.com").issuer, "foo.com") + self.assertRaises(ValueError, TOTP, KEY1, issuer="foo.com:bar") + + #============================================================================= + # using() tests + #============================================================================= + + # TODO: test using() w/ 'digits', 'alg', 'issue', 'wallet', **wallet_kwds + + def test_using_w_period(self): + """using() -- 'period' parameter""" + + # default + self.assertEqual(TOTP(KEY1).period, 30) + + # explicit value + self.assertEqual(TOTP.using(period=63)(KEY1).period, 63) + + # reject wrong type + self.assertRaises(TypeError, TOTP.using, period=1.5) + self.assertRaises(TypeError, TOTP.using, period='abc') + + # reject non-positive values + self.assertRaises(ValueError, TOTP.using, period=0) + self.assertRaises(ValueError, TOTP.using, period=-1) + + def test_using_w_now(self): + """using -- 'now' parameter""" + + # NOTE: reading time w/ normalize_time() to make sure custom .now actually has effect. + + # default -- time.time + otp = self.randotp() + self.assertIs(otp.now, _time.time) + self.assertAlmostEqual(otp.normalize_time(None), int(_time.time())) + + # custom function + counter = [123.12] + def now(): + counter[0] += 1 + return counter[0] + otp = self.randotp(cls=TOTP.using(now=now)) + # NOTE: TOTP() constructor invokes this as part of test, using up counter values 124 & 125 + self.assertEqual(otp.normalize_time(None), 126) + self.assertEqual(otp.normalize_time(None), 127) + + # require callable + self.assertRaises(TypeError, TOTP.using, now=123) + + # require returns int/float + msg_re = r"now\(\) function must return non-negative" + self.assertRaisesRegex(AssertionError, msg_re, TOTP.using, now=lambda: 'abc') + + # require returns non-negative value + self.assertRaisesRegex(AssertionError, msg_re, TOTP.using, now=lambda: -1) + + #============================================================================= + # internal method tests + #============================================================================= + + def test_normalize_token_instance(self, otp=None): + """normalize_token() -- instance method""" + if otp is None: + otp = self.randotp(digits=7) + + # unicode & bytes + self.assertEqual(otp.normalize_token(u('1234567')), '1234567') + self.assertEqual(otp.normalize_token(b'1234567'), '1234567') + + # int + self.assertEqual(otp.normalize_token(1234567), '1234567') + + # int which needs 0 padding + self.assertEqual(otp.normalize_token(234567), '0234567') + + # reject wrong types (float, None) + self.assertRaises(TypeError, otp.normalize_token, 1234567.0) + self.assertRaises(TypeError, otp.normalize_token, None) + + # too few digits + self.assertRaises(exc.MalformedTokenError, otp.normalize_token, '123456') + + # too many digits + self.assertRaises(exc.MalformedTokenError, otp.normalize_token, '01234567') + self.assertRaises(exc.MalformedTokenError, otp.normalize_token, 12345678) + + def test_normalize_token_class(self): + """normalize_token() -- class method""" + self.test_normalize_token_instance(otp=TOTP.using(digits=7)) + + def test_normalize_time(self): + """normalize_time()""" + TotpFactory = TOTP.using() + otp = self.randotp(TotpFactory) + + for _ in range(10): + time = self.randtime() + tint = int(time) + + self.assertEqual(otp.normalize_time(time), tint) + self.assertEqual(otp.normalize_time(tint + 0.5), tint) + + self.assertEqual(otp.normalize_time(tint), tint) + + dt = datetime.datetime.utcfromtimestamp(time) + self.assertEqual(otp.normalize_time(dt), tint) + + orig = TotpFactory.now + try: + TotpFactory.now = staticmethod(lambda: time) + self.assertEqual(otp.normalize_time(None), tint) + finally: + TotpFactory.now = orig + + self.assertRaises(TypeError, otp.normalize_time, '1234') + + #============================================================================= + # key attr tests + #============================================================================= + + def test_key_attrs(self): + """pretty_key() and .key attributes""" + rng = self.getRandom() + + # test key attrs + otp = TOTP(KEY1_RAW, "raw") + self.assertEqual(otp.key, KEY1_RAW) + self.assertEqual(otp.hex_key, 'e01c630c2184b076ce99') + self.assertEqual(otp.base32_key, KEY1) + + # test pretty_key() + self.assertEqual(otp.pretty_key(), '4AOG-GDBB-QSYH-NTUZ') + self.assertEqual(otp.pretty_key(sep=" "), '4AOG GDBB QSYH NTUZ') + self.assertEqual(otp.pretty_key(sep=False), KEY1) + self.assertEqual(otp.pretty_key(format="hex"), 'e01c-630c-2184-b076-ce99') + + # quick fuzz test: make attr access works for random key & random size + otp = TOTP(new=True, size=rng.randint(10, 20)) + _ = otp.hex_key + _ = otp.base32_key + _ = otp.pretty_key() + + #============================================================================= + # generate() tests + #============================================================================= + def test_totp_token(self): + """generate() -- TotpToken() class""" + from passlib.totp import TOTP, TotpToken + + # test known set of values + otp = TOTP('s3jdvb7qd2r7jpxx') + result = otp.generate(1419622739) + self.assertIsInstance(result, TotpToken) + self.assertEqual(result.token, '897212') + self.assertEqual(result.counter, 47320757) + ##self.assertEqual(result.start_time, 1419622710) + self.assertEqual(result.expire_time, 1419622740) + self.assertEqual(result, ('897212', 1419622740)) + self.assertEqual(len(result), 2) + self.assertEqual(result[0], '897212') + self.assertEqual(result[1], 1419622740) + self.assertRaises(IndexError, result.__getitem__, -3) + self.assertRaises(IndexError, result.__getitem__, 2) + self.assertTrue(result) + + # time dependant bits... + otp.now = lambda : 1419622739.5 + self.assertEqual(result.remaining, 0.5) + self.assertTrue(result.valid) + + otp.now = lambda : 1419622741 + self.assertEqual(result.remaining, 0) + self.assertFalse(result.valid) + + # same time -- shouldn't return same object, but should be equal + result2 = otp.generate(1419622739) + self.assertIsNot(result2, result) + self.assertEqual(result2, result) + + # diff time in period -- shouldn't return same object, but should be equal + result3 = otp.generate(1419622711) + self.assertIsNot(result3, result) + self.assertEqual(result3, result) + + # shouldn't be equal + result4 = otp.generate(1419622999) + self.assertNotEqual(result4, result) + + def test_generate(self): + """generate()""" + from passlib.totp import TOTP + + # generate token + otp = TOTP(new=True) + time = self.randtime() + result = otp.generate(time) + token = result.token + self.assertIsInstance(token, unicode) + start_time = result.counter * 30 + + # should generate same token for next 29s + self.assertEqual(otp.generate(start_time + 29).token, token) + + # and new one at 30s + self.assertNotEqual(otp.generate(start_time + 30).token, token) + + # verify round-trip conversion of datetime + dt = datetime.datetime.utcfromtimestamp(time) + self.assertEqual(int(otp.normalize_time(dt)), int(time)) + + # handle datetime object + self.assertEqual(otp.generate(dt).token, token) + + # omitting value should use current time + otp2 = TOTP.using(now=lambda: time)(key=otp.base32_key) + self.assertEqual(otp2.generate().token, token) + + # reject invalid time + self.assertRaises(ValueError, otp.generate, -1) + + def test_generate_w_reference_vectors(self): + """generate() -- reference vectors""" + for otp, time, token, expires, prefix in self.iter_test_vectors(): + # should output correct token for specified time + result = otp.generate(time) + self.assertEqual(result.token, token, msg=prefix) + self.assertEqual(result.counter, time // otp.period, msg=prefix) + if expires: + self.assertEqual(result.expire_time, expires) + + #============================================================================= + # TotpMatch() tests + #============================================================================= + + def assertTotpMatch(self, match, time, skipped=0, period=30, window=30, msg=''): + from passlib.totp import TotpMatch + + # test type + self.assertIsInstance(match, TotpMatch) + + # totp sanity check + self.assertIsInstance(match.totp, TOTP) + self.assertEqual(match.totp.period, period) + + # test attrs + self.assertEqual(match.time, time, msg=msg + " matched time:") + expected = time // period + counter = expected + skipped + self.assertEqual(match.counter, counter, msg=msg + " matched counter:") + self.assertEqual(match.expected_counter, expected, msg=msg + " expected counter:") + self.assertEqual(match.skipped, skipped, msg=msg + " skipped:") + self.assertEqual(match.cache_seconds, period + window) + expire_time = (counter + 1) * period + self.assertEqual(match.expire_time, expire_time) + self.assertEqual(match.cache_time, expire_time + window) + + # test tuple + self.assertEqual(len(match), 2) + self.assertEqual(match, (counter, time)) + self.assertRaises(IndexError, match.__getitem__, -3) + self.assertEqual(match[0], counter) + self.assertEqual(match[1], time) + self.assertRaises(IndexError, match.__getitem__, 2) + + # test bool + self.assertTrue(match) + + def test_totp_match_w_valid_token(self): + """match() -- valid TotpMatch object""" + time = 141230981 + token = '781501' + otp = TOTP.using(now=lambda: time + 24 * 3600)(KEY3) + result = otp.match(token, time) + self.assertTotpMatch(result, time=time, skipped=0) + + def test_totp_match_w_older_token(self): + """match() -- valid TotpMatch object with future token""" + from passlib.totp import TotpMatch + + time = 141230981 + token = '781501' + otp = TOTP.using(now=lambda: time + 24 * 3600)(KEY3) + result = otp.match(token, time - 30) + self.assertTotpMatch(result, time=time - 30, skipped=1) + + def test_totp_match_w_new_token(self): + """match() -- valid TotpMatch object with past token""" + time = 141230981 + token = '781501' + otp = TOTP.using(now=lambda: time + 24 * 3600)(KEY3) + result = otp.match(token, time + 30) + self.assertTotpMatch(result, time=time + 30, skipped=-1) + + def test_totp_match_w_invalid_token(self): + """match() -- invalid TotpMatch object""" + time = 141230981 + token = '781501' + otp = TOTP.using(now=lambda: time + 24 * 3600)(KEY3) + self.assertRaises(exc.InvalidTokenError, otp.match, token, time + 60) + + #============================================================================= + # match() tests + #============================================================================= + + def assertVerifyMatches(self, expect_skipped, token, time, # * + otp, gen_time=None, **kwds): + """helper to test otp.match() output is correct""" + # NOTE: TotpMatch return type tested more throughly above ^^^ + msg = "key=%r alg=%r period=%r token=%r gen_time=%r time=%r:" % \ + (otp.base32_key, otp.alg, otp.period, token, gen_time, time) + result = otp.match(token, time, **kwds) + self.assertTotpMatch(result, + time=otp.normalize_time(time), + period=otp.period, + window=kwds.get("window", 30), + skipped=expect_skipped, + msg=msg) + + def assertVerifyRaises(self, exc_class, token, time, # * + otp, gen_time=None, + **kwds): + """helper to test otp.match() throws correct error""" + # NOTE: TotpMatch return type tested more throughly above ^^^ + msg = "key=%r alg=%r period=%r token=%r gen_time=%r time=%r:" % \ + (otp.base32_key, otp.alg, otp.period, token, gen_time, time) + return self.assertRaises(exc_class, otp.match, token, time, + __msg__=msg, **kwds) + + def test_match_w_window(self): + """match() -- 'time' and 'window' parameters""" + + # init generator & helper + otp = self.randotp() + period = otp.period + time = self.randtime() + token = otp.generate(time).token + common = dict(otp=otp, gen_time=time) + assertMatches = partial(self.assertVerifyMatches, **common) + assertRaises = partial(self.assertVerifyRaises, **common) + + #------------------------------- + # basic validation, and 'window' parameter + #------------------------------- + + # validate against previous counter (passes if window >= period) + assertRaises(exc.InvalidTokenError, token, time - period, window=0) + assertMatches(+1, token, time - period, window=period) + assertMatches(+1, token, time - period, window=2 * period) + + # validate against current counter + assertMatches(0, token, time, window=0) + + # validate against next counter (passes if window >= period) + assertRaises(exc.InvalidTokenError, token, time + period, window=0) + assertMatches(-1, token, time + period, window=period) + assertMatches(-1, token, time + period, window=2 * period) + + # validate against two time steps later (should never pass) + assertRaises(exc.InvalidTokenError, token, time + 2 * period, window=0) + assertRaises(exc.InvalidTokenError, token, time + 2 * period, window=period) + assertMatches(-2, token, time + 2 * period, window=2 * period) + + # TODO: test window values that aren't multiples of period + # (esp ensure counter rounding works correctly) + + #------------------------------- + # time normalization + #------------------------------- + + # handle datetimes + dt = datetime.datetime.utcfromtimestamp(time) + assertMatches(0, token, dt, window=0) + + # reject invalid time + assertRaises(ValueError, token, -1) + + def test_match_w_skew(self): + """match() -- 'skew' parameters""" + # init generator & helper + otp = self.randotp() + period = otp.period + time = self.randtime() + common = dict(otp=otp, gen_time=time) + assertMatches = partial(self.assertVerifyMatches, **common) + assertRaises = partial(self.assertVerifyRaises, **common) + + # assume client is running far behind server / has excessive transmission delay + skew = 3 * period + behind_token = otp.generate(time - skew).token + assertRaises(exc.InvalidTokenError, behind_token, time, window=0) + assertMatches(-3, behind_token, time, window=0, skew=-skew) + + # assume client is running far ahead of server + ahead_token = otp.generate(time + skew).token + assertRaises(exc.InvalidTokenError, ahead_token, time, window=0) + assertMatches(+3, ahead_token, time, window=0, skew=skew) + + # TODO: test skew + larger window + + def test_match_w_reuse(self): + """match() -- 'reuse' and 'last_counter' parameters""" + + # init generator & helper + otp = self.randotp() + period = otp.period + time = self.randtime() + tdata = otp.generate(time) + token = tdata.token + counter = tdata.counter + expire_time = tdata.expire_time + common = dict(otp=otp, gen_time=time) + assertMatches = partial(self.assertVerifyMatches, **common) + assertRaises = partial(self.assertVerifyRaises, **common) + + # last counter unset -- + # previous period's token should count as valid + assertMatches(-1, token, time + period, window=period) + + # last counter set 2 periods ago -- + # previous period's token should count as valid + assertMatches(-1, token, time + period, last_counter=counter-1, + window=period) + + # last counter set 2 periods ago -- + # 2 periods ago's token should NOT count as valid + assertRaises(exc.InvalidTokenError, token, time + 2 * period, + last_counter=counter, window=period) + + # last counter set 1 period ago -- + # previous period's token should now be rejected as 'used' + err = assertRaises(exc.UsedTokenError, token, time + period, + last_counter=counter, window=period) + self.assertEqual(err.expire_time, expire_time) + + # last counter set to current period -- + # current period's token should be rejected + err = assertRaises(exc.UsedTokenError, token, time, + last_counter=counter, window=0) + self.assertEqual(err.expire_time, expire_time) + + def test_match_w_token_normalization(self): + """match() -- token normalization""" + # setup test helper + otp = TOTP('otxl2f5cctbprpzx') + match = otp.match + time = 1412889861 + + # separators / spaces should be stripped (orig token '332136') + self.assertTrue(match(' 3 32-136 ', time)) + + # ascii bytes + self.assertTrue(match(b'332136', time)) + + # too few digits + self.assertRaises(exc.MalformedTokenError, match, '12345', time) + + # invalid char + self.assertRaises(exc.MalformedTokenError, match, '12345X', time) + + # leading zeros count towards size + self.assertRaises(exc.MalformedTokenError, match, '0123456', time) + + def test_match_w_reference_vectors(self): + """match() -- reference vectors""" + for otp, time, token, expires, msg in self.iter_test_vectors(): + # create wrapper + match = otp.match + + # token should match against time + result = match(token, time) + self.assertTrue(result) + self.assertEqual(result.counter, time // otp.period, msg=msg) + + # should NOT match against another time + self.assertRaises(exc.InvalidTokenError, match, token, time + 100, window=0) + + #============================================================================= + # verify() tests + #============================================================================= + def test_verify(self): + """verify()""" + # NOTE: since this is thin wrapper around .from_source() and .match(), + # just testing basic behavior here. + + from passlib.totp import TOTP + + time = 1412889861 + TotpFactory = TOTP.using(now=lambda: time) + + # successful match + source1 = dict(v=1, type="totp", key='otxl2f5cctbprpzx') + match = TotpFactory.verify('332136', source1) + self.assertTotpMatch(match, time=time) + + # failed match + source1 = dict(v=1, type="totp", key='otxl2f5cctbprpzx') + self.assertRaises(exc.InvalidTokenError, TotpFactory.verify, '332155', source1) + + # bad source + source1 = dict(v=1, type="totp") + self.assertRaises(ValueError, TotpFactory.verify, '332155', source1) + + # successful match -- json source + source1json = '{"v": 1, "type": "totp", "key": "otxl2f5cctbprpzx"}' + match = TotpFactory.verify('332136', source1json) + self.assertTotpMatch(match, time=time) + + # successful match -- URI + source1uri = 'otpauth://totp/Label?secret=otxl2f5cctbprpzx' + match = TotpFactory.verify('332136', source1uri) + self.assertTotpMatch(match, time=time) + + #============================================================================= + # serialization frontend tests + #============================================================================= + def test_from_source(self): + """from_source()""" + from passlib.totp import TOTP + from_source = TOTP.from_source + + # uri (unicode) + otp = from_source(u("otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&" + "issuer=Example")) + self.assertEqual(otp.key, KEY4_RAW) + + # uri (bytes) + otp = from_source(b"otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&" + b"issuer=Example") + self.assertEqual(otp.key, KEY4_RAW) + + # dict + otp = from_source(dict(v=1, type="totp", key=KEY4)) + self.assertEqual(otp.key, KEY4_RAW) + + # json (unicode) + otp = from_source(u('{"v": 1, "type": "totp", "key": "JBSWY3DPEHPK3PXP"}')) + self.assertEqual(otp.key, KEY4_RAW) + + # json (bytes) + otp = from_source(b'{"v": 1, "type": "totp", "key": "JBSWY3DPEHPK3PXP"}') + self.assertEqual(otp.key, KEY4_RAW) + + # TOTP object -- return unchanged + self.assertIs(from_source(otp), otp) + + # TOTP object w/ different wallet -- return new one. + wallet1 = AppWallet() + otp1 = TOTP.using(wallet=wallet1).from_source(otp) + self.assertIsNot(otp1, otp) + self.assertEqual(otp1.to_dict(), otp.to_dict()) + + # TOTP object w/ same wallet -- return original + otp2 = TOTP.using(wallet=wallet1).from_source(otp1) + self.assertIs(otp2, otp1) + + # random string + self.assertRaises(ValueError, from_source, u("foo")) + self.assertRaises(ValueError, from_source, b"foo") + + #============================================================================= + # uri serialization tests + #============================================================================= + def test_from_uri(self): + """from_uri()""" + from passlib.totp import TOTP + from_uri = TOTP.from_uri + + # URIs from https://code.google.com/p/google-authenticator/wiki/KeyUriFormat + + #-------------------------------------------------------------------------------- + # canonical uri + #-------------------------------------------------------------------------------- + otp = from_uri("otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&" + "issuer=Example") + self.assertIsInstance(otp, TOTP) + self.assertEqual(otp.key, KEY4_RAW) + self.assertEqual(otp.label, "alice@google.com") + self.assertEqual(otp.issuer, "Example") + self.assertEqual(otp.alg, "sha1") # default + self.assertEqual(otp.period, 30) # default + self.assertEqual(otp.digits, 6) # default + + #-------------------------------------------------------------------------------- + # secret param + #-------------------------------------------------------------------------------- + + # secret case insensitive + otp = from_uri("otpauth://totp/Example:alice@google.com?secret=jbswy3dpehpk3pxp&" + "issuer=Example") + self.assertEqual(otp.key, KEY4_RAW) + + # missing secret + self.assertRaises(ValueError, from_uri, "otpauth://totp/Example:alice@google.com?digits=6") + + # undecodable secret + self.assertRaises(Base32DecodeError, from_uri, "otpauth://totp/Example:alice@google.com?" + "secret=JBSWY3DPEHP@3PXP") + + #-------------------------------------------------------------------------------- + # label param + #-------------------------------------------------------------------------------- + + # w/ encoded space + otp = from_uri("otpauth://totp/Provider1:Alice%20Smith?secret=JBSWY3DPEHPK3PXP&" + "issuer=Provider1") + self.assertEqual(otp.label, "Alice Smith") + self.assertEqual(otp.issuer, "Provider1") + + # w/ encoded space and colon + # (note url has leading space before 'alice') -- taken from KeyURI spec + otp = from_uri("otpauth://totp/Big%20Corporation%3A%20alice@bigco.com?" + "secret=JBSWY3DPEHPK3PXP") + self.assertEqual(otp.label, "alice@bigco.com") + self.assertEqual(otp.issuer, "Big Corporation") + + #-------------------------------------------------------------------------------- + # issuer param / prefix + #-------------------------------------------------------------------------------- + + # 'new style' issuer only + otp = from_uri("otpauth://totp/alice@bigco.com?secret=JBSWY3DPEHPK3PXP&issuer=Big%20Corporation") + self.assertEqual(otp.label, "alice@bigco.com") + self.assertEqual(otp.issuer, "Big Corporation") + + # new-vs-old issuer mismatch + self.assertRaises(ValueError, TOTP.from_uri, + "otpauth://totp/Provider1:alice?secret=JBSWY3DPEHPK3PXP&issuer=Provider2") + + #-------------------------------------------------------------------------------- + # algorithm param + #-------------------------------------------------------------------------------- + + # custom alg + otp = from_uri("otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA256") + self.assertEqual(otp.alg, "sha256") + + # unknown alg + self.assertRaises(ValueError, from_uri, "otpauth://totp/Example:alice@google.com?" + "secret=JBSWY3DPEHPK3PXP&algorithm=SHA333") + + #-------------------------------------------------------------------------------- + # digit param + #-------------------------------------------------------------------------------- + + # custom digits + otp = from_uri("otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&digits=8") + self.assertEqual(otp.digits, 8) + + # digits out of range / invalid + self.assertRaises(ValueError, from_uri, "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&digits=A") + self.assertRaises(ValueError, from_uri, "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&digits=%20") + self.assertRaises(ValueError, from_uri, "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&digits=15") + + #-------------------------------------------------------------------------------- + # period param + #-------------------------------------------------------------------------------- + + # custom period + otp = from_uri("otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&period=63") + self.assertEqual(otp.period, 63) + + # reject period < 1 + self.assertRaises(ValueError, from_uri, "otpauth://totp/Example:alice@google.com?" + "secret=JBSWY3DPEHPK3PXP&period=0") + + self.assertRaises(ValueError, from_uri, "otpauth://totp/Example:alice@google.com?" + "secret=JBSWY3DPEHPK3PXP&period=-1") + + #-------------------------------------------------------------------------------- + # unrecognized param + #-------------------------------------------------------------------------------- + + # should issue warning, but otherwise ignore extra param + with self.assertWarningList([ + dict(category=exc.PasslibRuntimeWarning, message_re="unexpected parameters encountered") + ]): + otp = from_uri("otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&" + "foo=bar&period=63") + self.assertEqual(otp.base32_key, KEY4) + self.assertEqual(otp.period, 63) + + def test_to_uri(self): + """to_uri()""" + + #------------------------------------------------------------------------- + # label & issuer parameters + #------------------------------------------------------------------------- + + # with label & issuer + otp = TOTP(KEY4, alg="sha1", digits=6, period=30) + self.assertEqual(otp.to_uri("alice@google.com", "Example Org"), + "otpauth://totp/Example%20Org:alice@google.com?secret=JBSWY3DPEHPK3PXP&" + "issuer=Example%20Org") + + # label is required + self.assertRaises(ValueError, otp.to_uri, None, "Example Org") + + # with label only + self.assertEqual(otp.to_uri("alice@google.com"), + "otpauth://totp/alice@google.com?secret=JBSWY3DPEHPK3PXP") + + # with default label from constructor + otp.label = "alice@google.com" + self.assertEqual(otp.to_uri(), + "otpauth://totp/alice@google.com?secret=JBSWY3DPEHPK3PXP") + + # with default label & default issuer from constructor + otp.issuer = "Example Org" + self.assertEqual(otp.to_uri(), + "otpauth://totp/Example%20Org:alice@google.com?secret=JBSWY3DPEHPK3PXP" + "&issuer=Example%20Org") + + # reject invalid label + self.assertRaises(ValueError, otp.to_uri, "label:with:semicolons") + + # reject invalid issuer + self.assertRaises(ValueError, otp.to_uri, "alice@google.com", "issuer:with:semicolons") + + #------------------------------------------------------------------------- + # algorithm parameter + #------------------------------------------------------------------------- + self.assertEqual(TOTP(KEY4, alg="sha256").to_uri("alice@google.com"), + "otpauth://totp/alice@google.com?secret=JBSWY3DPEHPK3PXP&" + "algorithm=SHA256") + + #------------------------------------------------------------------------- + # digits parameter + #------------------------------------------------------------------------- + self.assertEqual(TOTP(KEY4, digits=8).to_uri("alice@google.com"), + "otpauth://totp/alice@google.com?secret=JBSWY3DPEHPK3PXP&" + "digits=8") + + #------------------------------------------------------------------------- + # period parameter + #------------------------------------------------------------------------- + self.assertEqual(TOTP(KEY4, period=63).to_uri("alice@google.com"), + "otpauth://totp/alice@google.com?secret=JBSWY3DPEHPK3PXP&" + "period=63") + + #============================================================================= + # dict serialization tests + #============================================================================= + def test_from_dict(self): + """from_dict()""" + from passlib.totp import TOTP + from_dict = TOTP.from_dict + + #-------------------------------------------------------------------------------- + # canonical simple example + #-------------------------------------------------------------------------------- + otp = from_dict(dict(v=1, type="totp", key=KEY4, label="alice@google.com", issuer="Example")) + self.assertIsInstance(otp, TOTP) + self.assertEqual(otp.key, KEY4_RAW) + self.assertEqual(otp.label, "alice@google.com") + self.assertEqual(otp.issuer, "Example") + self.assertEqual(otp.alg, "sha1") # default + self.assertEqual(otp.period, 30) # default + self.assertEqual(otp.digits, 6) # default + + #-------------------------------------------------------------------------------- + # metadata + #-------------------------------------------------------------------------------- + + # missing version + self.assertRaises(ValueError, from_dict, dict(type="totp", key=KEY4)) + + # invalid version + self.assertRaises(ValueError, from_dict, dict(v=0, type="totp", key=KEY4)) + self.assertRaises(ValueError, from_dict, dict(v=999, type="totp", key=KEY4)) + + # missing type + self.assertRaises(ValueError, from_dict, dict(v=1, key=KEY4)) + + #-------------------------------------------------------------------------------- + # secret param + #-------------------------------------------------------------------------------- + + # secret case insensitive + otp = from_dict(dict(v=1, type="totp", key=KEY4.lower(), label="alice@google.com", issuer="Example")) + self.assertEqual(otp.key, KEY4_RAW) + + # missing secret + self.assertRaises(ValueError, from_dict, dict(v=1, type="totp")) + + # undecodable secret + self.assertRaises(Base32DecodeError, from_dict, + dict(v=1, type="totp", key="JBSWY3DPEHP@3PXP")) + + #-------------------------------------------------------------------------------- + # label & issuer params + #-------------------------------------------------------------------------------- + + otp = from_dict(dict(v=1, type="totp", key=KEY4, label="Alice Smith", issuer="Provider1")) + self.assertEqual(otp.label, "Alice Smith") + self.assertEqual(otp.issuer, "Provider1") + + #-------------------------------------------------------------------------------- + # algorithm param + #-------------------------------------------------------------------------------- + + # custom alg + otp = from_dict(dict(v=1, type="totp", key=KEY4, alg="sha256")) + self.assertEqual(otp.alg, "sha256") + + # unknown alg + self.assertRaises(ValueError, from_dict, dict(v=1, type="totp", key=KEY4, alg="sha333")) + + #-------------------------------------------------------------------------------- + # digit param + #-------------------------------------------------------------------------------- + + # custom digits + otp = from_dict(dict(v=1, type="totp", key=KEY4, digits=8)) + self.assertEqual(otp.digits, 8) + + # digits out of range / invalid + self.assertRaises(TypeError, from_dict, dict(v=1, type="totp", key=KEY4, digits="A")) + self.assertRaises(ValueError, from_dict, dict(v=1, type="totp", key=KEY4, digits=15)) + + #-------------------------------------------------------------------------------- + # period param + #-------------------------------------------------------------------------------- + + # custom period + otp = from_dict(dict(v=1, type="totp", key=KEY4, period=63)) + self.assertEqual(otp.period, 63) + + # reject period < 1 + self.assertRaises(ValueError, from_dict, dict(v=1, type="totp", key=KEY4, period=0)) + self.assertRaises(ValueError, from_dict, dict(v=1, type="totp", key=KEY4, period=-1)) + + #-------------------------------------------------------------------------------- + # unrecognized param + #-------------------------------------------------------------------------------- + self.assertRaises(TypeError, from_dict, dict(v=1, type="totp", key=KEY4, INVALID=123)) + + def test_to_dict(self): + """to_dict()""" + + #------------------------------------------------------------------------- + # label & issuer parameters + #------------------------------------------------------------------------- + + # without label or issuer + otp = TOTP(KEY4, alg="sha1", digits=6, period=30) + self.assertEqual(otp.to_dict(), dict(v=1, type="totp", key=KEY4)) + + # with label & issuer from constructor + otp = TOTP(KEY4, alg="sha1", digits=6, period=30, + label="alice@google.com", issuer="Example Org") + self.assertEqual(otp.to_dict(), + dict(v=1, type="totp", key=KEY4, + label="alice@google.com", issuer="Example Org")) + + # with label only + otp = TOTP(KEY4, alg="sha1", digits=6, period=30, + label="alice@google.com") + self.assertEqual(otp.to_dict(), + dict(v=1, type="totp", key=KEY4, + label="alice@google.com")) + + # with issuer only + otp = TOTP(KEY4, alg="sha1", digits=6, period=30, + issuer="Example Org") + self.assertEqual(otp.to_dict(), + dict(v=1, type="totp", key=KEY4, + issuer="Example Org")) + + # don't serialize default issuer + TotpFactory = TOTP.using(issuer="Example Org") + otp = TotpFactory(KEY4) + self.assertEqual(otp.to_dict(), dict(v=1, type="totp", key=KEY4)) + + # don't serialize default issuer *even if explicitly set* + otp = TotpFactory(KEY4, issuer="Example Org") + self.assertEqual(otp.to_dict(), dict(v=1, type="totp", key=KEY4)) + + #------------------------------------------------------------------------- + # algorithm parameter + #------------------------------------------------------------------------- + self.assertEqual(TOTP(KEY4, alg="sha256").to_dict(), + dict(v=1, type="totp", key=KEY4, alg="sha256")) + + #------------------------------------------------------------------------- + # digits parameter + #------------------------------------------------------------------------- + self.assertEqual(TOTP(KEY4, digits=8).to_dict(), + dict(v=1, type="totp", key=KEY4, digits=8)) + + #------------------------------------------------------------------------- + # period parameter + #------------------------------------------------------------------------- + self.assertEqual(TOTP(KEY4, period=63).to_dict(), + dict(v=1, type="totp", key=KEY4, period=63)) + + # TODO: to_dict() + # with encrypt=False + # with encrypt="auto" + wallet + secrets + # with encrypt="auto" + wallet + no secrets + # with encrypt="auto" + no wallet + # with encrypt=True + wallet + secrets + # with encrypt=True + wallet + no secrets + # with encrypt=True + no wallet + # that 'changed' is set for old versions, and old encryption tags. + + #============================================================================= + # json serialization tests + #============================================================================= + + # TODO: from_json() / to_json(). + # (skipped for right now cause just wrapper for from_dict/to_dict) + + #============================================================================= + # eoc + #============================================================================= + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/test_utils.py b/ansible/lib/python3.11/site-packages/passlib/tests/test_utils.py new file mode 100644 index 000000000..59ba160f2 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/tests/test_utils.py @@ -0,0 +1,1171 @@ +"""tests for passlib.util""" +#============================================================================= +# imports +#============================================================================= +from __future__ import with_statement +# core +from functools import partial +import warnings +# site +# pkg +# module +from passlib.utils import is_ascii_safe, to_bytes +from passlib.utils.compat import irange, PY2, PY3, u, unicode, join_bytes, PYPY +from passlib.tests.utils import TestCase, hb, run_with_fixed_seeds + +#============================================================================= +# byte funcs +#============================================================================= +class MiscTest(TestCase): + """tests various parts of utils module""" + + # NOTE: could test xor_bytes(), but it's exercised well enough by pbkdf2 test + + def test_compat(self): + """test compat's lazymodule""" + from passlib.utils import compat + # "" + self.assertRegex(repr(compat), + r"^$") + + # test synthentic dir() + dir(compat) + self.assertTrue('UnicodeIO' in dir(compat)) + self.assertTrue('irange' in dir(compat)) + + def test_classproperty(self): + from passlib.utils.decor import classproperty + + class test(object): + xvar = 1 + @classproperty + def xprop(cls): + return cls.xvar + + self.assertEqual(test.xprop, 1) + prop = test.__dict__['xprop'] + self.assertIs(prop.im_func, prop.__func__) + + def test_deprecated_function(self): + from passlib.utils.decor import deprecated_function + # NOTE: not comprehensive, just tests the basic behavior + + @deprecated_function(deprecated="1.6", removed="1.8") + def test_func(*args): + """test docstring""" + return args + + self.assertTrue(".. deprecated::" in test_func.__doc__) + + with self.assertWarningList(dict(category=DeprecationWarning, + message="the function passlib.tests.test_utils.test_func() " + "is deprecated as of Passlib 1.6, and will be " + "removed in Passlib 1.8." + )): + self.assertEqual(test_func(1,2), (1,2)) + + def test_memoized_property(self): + from passlib.utils.decor import memoized_property + + class dummy(object): + counter = 0 + + @memoized_property + def value(self): + value = self.counter + self.counter = value+1 + return value + + d = dummy() + self.assertEqual(d.value, 0) + self.assertEqual(d.value, 0) + self.assertEqual(d.counter, 1) + + prop = dummy.value + if not PY3: + self.assertIs(prop.im_func, prop.__func__) + + def test_getrandbytes(self): + """getrandbytes()""" + from passlib.utils import getrandbytes + wrapper = partial(getrandbytes, self.getRandom()) + self.assertEqual(len(wrapper(0)), 0) + a = wrapper(10) + b = wrapper(10) + self.assertIsInstance(a, bytes) + self.assertEqual(len(a), 10) + self.assertEqual(len(b), 10) + self.assertNotEqual(a, b) + + @run_with_fixed_seeds(count=1024) + def test_getrandstr(self, seed): + """getrandstr()""" + from passlib.utils import getrandstr + + wrapper = partial(getrandstr, self.getRandom(seed=seed)) + + # count 0 + self.assertEqual(wrapper('abc',0), '') + + # count <0 + self.assertRaises(ValueError, wrapper, 'abc', -1) + + # letters 0 + self.assertRaises(ValueError, wrapper, '', 0) + + # letters 1 + self.assertEqual(wrapper('a', 5), 'aaaaa') + + # NOTE: the following parts are non-deterministic, + # with a small chance of failure (outside chance it may pick + # a string w/o one char, even more remote chance of picking + # same string). to combat this, we run it against multiple + # fixed seeds (using run_with_fixed_seeds decorator), + # and hope that they're sufficient to test the range of behavior. + + # letters + x = wrapper(u('abc'), 32) + y = wrapper(u('abc'), 32) + self.assertIsInstance(x, unicode) + self.assertNotEqual(x,y) + self.assertEqual(sorted(set(x)), [u('a'),u('b'),u('c')]) + + # bytes + x = wrapper(b'abc', 32) + y = wrapper(b'abc', 32) + self.assertIsInstance(x, bytes) + self.assertNotEqual(x,y) + # NOTE: decoding this due to py3 bytes + self.assertEqual(sorted(set(x.decode("ascii"))), [u('a'),u('b'),u('c')]) + + def test_generate_password(self): + """generate_password()""" + from passlib.utils import generate_password + warnings.filterwarnings("ignore", "The function.*generate_password\(\) is deprecated") + self.assertEqual(len(generate_password(15)), 15) + + def test_is_crypt_context(self): + """test is_crypt_context()""" + from passlib.utils import is_crypt_context + from passlib.context import CryptContext + cc = CryptContext(["des_crypt"]) + self.assertTrue(is_crypt_context(cc)) + self.assertFalse(not is_crypt_context(cc)) + + def test_genseed(self): + """test genseed()""" + import random + from passlib.utils import genseed + rng = random.Random(genseed()) + a = rng.randint(0, 10**10) + + rng = random.Random(genseed()) + b = rng.randint(0, 10**10) + + self.assertNotEqual(a,b) + + rng.seed(genseed(rng)) + + def test_crypt(self): + """test crypt.crypt() wrappers""" + from passlib.utils import has_crypt, safe_crypt, test_crypt + from passlib.registry import get_supported_os_crypt_schemes, get_crypt_handler + + # test everything is disabled + supported = get_supported_os_crypt_schemes() + if not has_crypt: + self.assertEqual(supported, ()) + self.assertEqual(safe_crypt("test", "aa"), None) + self.assertFalse(test_crypt("test", "aaqPiZY5xR5l.")) # des_crypt() hash of "test" + raise self.skipTest("crypt.crypt() not available") + + # expect there to be something supported, if crypt() is present + if not supported: + # NOTE: failures here should be investigated. usually means one of: + # 1) at least one of passlib's os_crypt detection routines is giving false negative + # 2) crypt() ONLY supports some hash alg which passlib doesn't know about + # 3) crypt() is present but completely disabled (never encountered this yet) + raise self.fail("crypt() present, but no supported schemes found!") + + # pick cheap alg if possible, with minimum rounds, to speed up this test. + # NOTE: trusting hasher class works properly (should have been verified using it's own UTs) + for scheme in ("md5_crypt", "sha256_crypt"): + if scheme in supported: + break + else: + scheme = supported[-1] + hasher = get_crypt_handler(scheme) + if getattr(hasher, "min_rounds", None): + hasher = hasher.using(rounds=hasher.min_rounds) + + # helpers to generate hashes & config strings to work with + def get_hash(secret): + assert isinstance(secret, unicode) + hash = hasher.hash(secret) + if isinstance(hash, bytes): # py2 + hash = hash.decode("utf-8") + assert isinstance(hash, unicode) + return hash + + # test ascii password & return type + s1 = u("test") + h1 = get_hash(s1) + result = safe_crypt(s1, h1) + self.assertIsInstance(result, unicode) + self.assertEqual(result, h1) + self.assertEqual(safe_crypt(to_bytes(s1), to_bytes(h1)), h1) + + # make sure crypt doesn't just blindly return h1 for whatever we pass in + h1x = h1[:-2] + 'xx' + self.assertEqual(safe_crypt(s1, h1x), h1) + + # test utf-8 / unicode password + s2 = u('test\u1234') + h2 = get_hash(s2) + self.assertEqual(safe_crypt(s2, h2), h2) + self.assertEqual(safe_crypt(to_bytes(s2), to_bytes(h2)), h2) + + # test rejects null chars in password + self.assertRaises(ValueError, safe_crypt, '\x00', h1) + + # check test_crypt() + self.assertTrue(test_crypt("test", h1)) + self.assertFalse(test_crypt("test", h1x)) + + # check crypt returning variant error indicators + # some platforms return None on errors, others empty string, + # The BSDs in some cases return ":" + import passlib.utils as mod + orig = mod._crypt + try: + retval = None + mod._crypt = lambda secret, hash: retval + + for retval in [None, "", ":", ":0", "*0"]: + self.assertEqual(safe_crypt("test", h1), None) + self.assertFalse(test_crypt("test", h1)) + + retval = 'xxx' + self.assertEqual(safe_crypt("test", h1), "xxx") + self.assertFalse(test_crypt("test", h1)) + + finally: + mod._crypt = orig + + def test_consteq(self): + """test consteq()""" + # NOTE: this test is kind of over the top, but that's only because + # this is used for the critical task of comparing hashes for equality. + from passlib.utils import consteq, str_consteq + + # ensure error raises for wrong types + self.assertRaises(TypeError, consteq, u(''), b'') + self.assertRaises(TypeError, consteq, u(''), 1) + self.assertRaises(TypeError, consteq, u(''), None) + + self.assertRaises(TypeError, consteq, b'', u('')) + self.assertRaises(TypeError, consteq, b'', 1) + self.assertRaises(TypeError, consteq, b'', None) + + self.assertRaises(TypeError, consteq, None, u('')) + self.assertRaises(TypeError, consteq, None, b'') + self.assertRaises(TypeError, consteq, 1, u('')) + self.assertRaises(TypeError, consteq, 1, b'') + + def consteq_supports_string(value): + # under PY2, it supports all unicode strings (when present at all), + # under PY3, compare_digest() only supports ascii unicode strings. + # confirmed for: cpython 2.7.9, cpython 3.4, pypy, pypy3, pyston + return (consteq is str_consteq or PY2 or is_ascii_safe(value)) + + # check equal inputs compare correctly + for value in [ + u("a"), + u("abc"), + u("\xff\xa2\x12\x00")*10, + ]: + if consteq_supports_string(value): + self.assertTrue(consteq(value, value), "value %r:" % (value,)) + else: + self.assertRaises(TypeError, consteq, value, value) + self.assertTrue(str_consteq(value, value), "value %r:" % (value,)) + + value = value.encode("latin-1") + self.assertTrue(consteq(value, value), "value %r:" % (value,)) + + # check non-equal inputs compare correctly + for l,r in [ + # check same-size comparisons with differing contents fail. + (u("a"), u("c")), + (u("abcabc"), u("zbaabc")), + (u("abcabc"), u("abzabc")), + (u("abcabc"), u("abcabz")), + ((u("\xff\xa2\x12\x00")*10)[:-1] + u("\x01"), + u("\xff\xa2\x12\x00")*10), + + # check different-size comparisons fail. + (u(""), u("a")), + (u("abc"), u("abcdef")), + (u("abc"), u("defabc")), + (u("qwertyuiopasdfghjklzxcvbnm"), u("abc")), + ]: + if consteq_supports_string(l) and consteq_supports_string(r): + self.assertFalse(consteq(l, r), "values %r %r:" % (l,r)) + self.assertFalse(consteq(r, l), "values %r %r:" % (r,l)) + else: + self.assertRaises(TypeError, consteq, l, r) + self.assertRaises(TypeError, consteq, r, l) + self.assertFalse(str_consteq(l, r), "values %r %r:" % (l,r)) + self.assertFalse(str_consteq(r, l), "values %r %r:" % (r,l)) + + l = l.encode("latin-1") + r = r.encode("latin-1") + self.assertFalse(consteq(l, r), "values %r %r:" % (l,r)) + self.assertFalse(consteq(r, l), "values %r %r:" % (r,l)) + + # TODO: add some tests to ensure we take THETA(strlen) time. + # this might be hard to do reproducably. + # NOTE: below code was used to generate stats for analysis + ##from math import log as logb + ##import timeit + ##multipliers = [ 1< encode() -> decode() -> raw + # + + # generate some random bytes + size = rng.randint(1 if saw_zero else 0, 12) + if not size: + saw_zero = True + enc_size = (4*size+2)//3 + raw = getrandbytes(rng, size) + + # encode them, check invariants + encoded = engine.encode_bytes(raw) + self.assertEqual(len(encoded), enc_size) + + # make sure decode returns original + result = engine.decode_bytes(encoded) + self.assertEqual(result, raw) + + # + # test encoded -> decode() -> encode() -> encoded + # + + # generate some random encoded data + if size % 4 == 1: + size += rng.choice([-1,1,2]) + raw_size = 3*size//4 + encoded = getrandstr(rng, engine.bytemap, size) + + # decode them, check invariants + raw = engine.decode_bytes(encoded) + self.assertEqual(len(raw), raw_size, "encoded %d:" % size) + + # make sure encode returns original (barring padding bits) + result = engine.encode_bytes(raw) + if size % 4: + self.assertEqual(result[:-1], encoded[:-1]) + else: + self.assertEqual(result, encoded) + + def test_repair_unused(self): + """test repair_unused()""" + # NOTE: this test relies on encode_bytes() always returning clear + # padding bits - which should be ensured by test vectors. + from passlib.utils import getrandstr + rng = self.getRandom() + engine = self.engine + check_repair_unused = self.engine.check_repair_unused + i = 0 + while i < 300: + size = rng.randint(0,23) + cdata = getrandstr(rng, engine.charmap, size).encode("ascii") + if size & 3 == 1: + # should throw error + self.assertRaises(ValueError, check_repair_unused, cdata) + continue + rdata = engine.encode_bytes(engine.decode_bytes(cdata)) + if rng.random() < .5: + cdata = cdata.decode("ascii") + rdata = rdata.decode("ascii") + if cdata == rdata: + # should leave unchanged + ok, result = check_repair_unused(cdata) + self.assertFalse(ok) + self.assertEqual(result, rdata) + else: + # should repair bits + self.assertNotEqual(size % 4, 0) + ok, result = check_repair_unused(cdata) + self.assertTrue(ok) + self.assertEqual(result, rdata) + i += 1 + + #=================================================================== + # test transposed encode/decode - encoding independant + #=================================================================== + # NOTE: these tests assume normal encode/decode has been tested elsewhere. + + transposed = [ + # orig, result, transpose map + (b"\x33\x22\x11", b"\x11\x22\x33",[2,1,0]), + (b"\x22\x33\x11", b"\x11\x22\x33",[1,2,0]), + ] + + transposed_dups = [ + # orig, result, transpose projection + (b"\x11\x11\x22", b"\x11\x22\x33",[0,0,1]), + ] + + def test_encode_transposed_bytes(self): + """test encode_transposed_bytes()""" + engine = self.engine + for result, input, offsets in self.transposed + self.transposed_dups: + tmp = engine.encode_transposed_bytes(input, offsets) + out = engine.decode_bytes(tmp) + self.assertEqual(out, result) + + self.assertRaises(TypeError, engine.encode_transposed_bytes, u("a"), []) + + def test_decode_transposed_bytes(self): + """test decode_transposed_bytes()""" + engine = self.engine + for input, result, offsets in self.transposed: + tmp = engine.encode_bytes(input) + out = engine.decode_transposed_bytes(tmp, offsets) + self.assertEqual(out, result) + + def test_decode_transposed_bytes_bad(self): + """test decode_transposed_bytes() fails if map is a one-way""" + engine = self.engine + for input, _, offsets in self.transposed_dups: + tmp = engine.encode_bytes(input) + self.assertRaises(TypeError, engine.decode_transposed_bytes, tmp, + offsets) + + #=================================================================== + # test 6bit handling + #=================================================================== + def check_int_pair(self, bits, encoded_pairs): + """helper to check encode_intXX & decode_intXX functions""" + rng = self.getRandom() + engine = self.engine + encode = getattr(engine, "encode_int%s" % bits) + decode = getattr(engine, "decode_int%s" % bits) + pad = -bits % 6 + chars = (bits+pad)//6 + upper = 1< block_size, and wrong type + self.assertRaises(ValueError, helper, keylen=-1) + self.assertRaises(ValueError, helper, keylen=17, hash='md5') + self.assertRaises(TypeError, helper, keylen='1') + +#============================================================================= +# test PBKDF2 support +#============================================================================= +class Pbkdf2_Test(TestCase): + """test pbkdf2() support""" + descriptionPrefix = "passlib.utils.pbkdf2.pbkdf2()" + + pbkdf2_test_vectors = [ + # (result, secret, salt, rounds, keylen, prf="sha1") + + # + # from rfc 3962 + # + + # test case 1 / 128 bit + ( + hb("cdedb5281bb2f801565a1122b2563515"), + b"password", b"ATHENA.MIT.EDUraeburn", 1, 16 + ), + + # test case 2 / 128 bit + ( + hb("01dbee7f4a9e243e988b62c73cda935d"), + b"password", b"ATHENA.MIT.EDUraeburn", 2, 16 + ), + + # test case 2 / 256 bit + ( + hb("01dbee7f4a9e243e988b62c73cda935da05378b93244ec8f48a99e61ad799d86"), + b"password", b"ATHENA.MIT.EDUraeburn", 2, 32 + ), + + # test case 3 / 256 bit + ( + hb("5c08eb61fdf71e4e4ec3cf6ba1f5512ba7e52ddbc5e5142f708a31e2e62b1e13"), + b"password", b"ATHENA.MIT.EDUraeburn", 1200, 32 + ), + + # test case 4 / 256 bit + ( + hb("d1daa78615f287e6a1c8b120d7062a493f98d203e6be49a6adf4fa574b6e64ee"), + b"password", b'\x12\x34\x56\x78\x78\x56\x34\x12', 5, 32 + ), + + # test case 5 / 256 bit + ( + hb("139c30c0966bc32ba55fdbf212530ac9c5ec59f1a452f5cc9ad940fea0598ed1"), + b"X"*64, b"pass phrase equals block size", 1200, 32 + ), + + # test case 6 / 256 bit + ( + hb("9ccad6d468770cd51b10e6a68721be611a8b4d282601db3b36be9246915ec82a"), + b"X"*65, b"pass phrase exceeds block size", 1200, 32 + ), + + # + # from rfc 6070 + # + ( + hb("0c60c80f961f0e71f3a9b524af6012062fe037a6"), + b"password", b"salt", 1, 20, + ), + + ( + hb("ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957"), + b"password", b"salt", 2, 20, + ), + + ( + hb("4b007901b765489abead49d926f721d065a429c1"), + b"password", b"salt", 4096, 20, + ), + + # just runs too long - could enable if ALL option is set + ##( + ## + ## unhexlify("eefe3d61cd4da4e4e9945b3d6ba2158c2634e984"), + ## "password", "salt", 16777216, 20, + ##), + + ( + hb("3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038"), + b"passwordPASSWORDpassword", + b"saltSALTsaltSALTsaltSALTsaltSALTsalt", + 4096, 25, + ), + + ( + hb("56fa6aa75548099dcc37d7f03425e0c3"), + b"pass\00word", b"sa\00lt", 4096, 16, + ), + + # + # from example in http://grub.enbug.org/Authentication + # + ( + hb("887CFF169EA8335235D8004242AA7D6187A41E3187DF0CE14E256D85ED" + "97A97357AAA8FF0A3871AB9EEFF458392F462F495487387F685B7472FC" + "6C29E293F0A0"), + b"hello", + hb("9290F727ED06C38BA4549EF7DE25CF5642659211B7FC076F2D28FEFD71" + "784BB8D8F6FB244A8CC5C06240631B97008565A120764C0EE9C2CB0073" + "994D79080136"), + 10000, 64, "hmac-sha512" + ), + + # + # custom + # + ( + hb('e248fb6b13365146f8ac6307cc222812'), + b"secret", b"salt", 10, 16, "hmac-sha1", + ), + ( + hb('e248fb6b13365146f8ac6307cc2228127872da6d'), + b"secret", b"salt", 10, None, "hmac-sha1", + ), + + ] + + def setUp(self): + super(Pbkdf2_Test, self).setUp() + warnings.filterwarnings("ignore", ".*passlib.utils.pbkdf2.*deprecated", DeprecationWarning) + + def test_known(self): + """test reference vectors""" + from passlib.utils.pbkdf2 import pbkdf2 + for row in self.pbkdf2_test_vectors: + correct, secret, salt, rounds, keylen = row[:5] + prf = row[5] if len(row) == 6 else "hmac-sha1" + result = pbkdf2(secret, salt, rounds, keylen, prf) + self.assertEqual(result, correct) + + def test_border(self): + """test border cases""" + from passlib.utils.pbkdf2 import pbkdf2 + def helper(secret=b'password', salt=b'salt', rounds=1, keylen=None, prf="hmac-sha1"): + return pbkdf2(secret, salt, rounds, keylen, prf) + helper() + + # invalid rounds + self.assertRaises(ValueError, helper, rounds=-1) + self.assertRaises(ValueError, helper, rounds=0) + self.assertRaises(TypeError, helper, rounds='x') + + # invalid keylen + self.assertRaises(ValueError, helper, keylen=-1) + self.assertRaises(ValueError, helper, keylen=0) + helper(keylen=1) + self.assertRaises(OverflowError, helper, keylen=20*(2**32-1)+1) + self.assertRaises(TypeError, helper, keylen='x') + + # invalid secret/salt type + self.assertRaises(TypeError, helper, salt=5) + self.assertRaises(TypeError, helper, secret=5) + + # invalid hash + self.assertRaises(ValueError, helper, prf='hmac-foo') + self.assertRaises(NotImplementedError, helper, prf='foo') + self.assertRaises(TypeError, helper, prf=5) + + def test_default_keylen(self): + """test keylen==None""" + from passlib.utils.pbkdf2 import pbkdf2 + def helper(secret=b'password', salt=b'salt', rounds=1, keylen=None, prf="hmac-sha1"): + return pbkdf2(secret, salt, rounds, keylen, prf) + self.assertEqual(len(helper(prf='hmac-sha1')), 20) + self.assertEqual(len(helper(prf='hmac-sha256')), 32) + + def test_custom_prf(self): + """test custom prf function""" + from passlib.utils.pbkdf2 import pbkdf2 + def prf(key, msg): + return hashlib.md5(key+msg+b'fooey').digest() + self.assertRaises(NotImplementedError, pbkdf2, b'secret', b'salt', 1000, 20, prf) + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/test_win32.py b/ansible/lib/python3.11/site-packages/passlib/tests/test_win32.py new file mode 100644 index 000000000..e818b62b9 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/tests/test_win32.py @@ -0,0 +1,50 @@ +"""tests for passlib.win32 -- (c) Assurance Technologies 2003-2009""" +#============================================================================= +# imports +#============================================================================= +# core +import warnings +# site +# pkg +from passlib.tests.utils import TestCase +# module +from passlib.utils.compat import u + +#============================================================================= +# +#============================================================================= +class UtilTest(TestCase): + """test util funcs in passlib.win32""" + + ##test hashes from http://msdn.microsoft.com/en-us/library/cc245828(v=prot.10).aspx + ## among other places + + def setUp(self): + super(UtilTest, self).setUp() + warnings.filterwarnings("ignore", + "the 'passlib.win32' module is deprecated") + + def test_lmhash(self): + from passlib.win32 import raw_lmhash + for secret, hash in [ + ("OLDPASSWORD", u("c9b81d939d6fd80cd408e6b105741864")), + ("NEWPASSWORD", u('09eeab5aa415d6e4d408e6b105741864')), + ("welcome", u("c23413a8a1e7665faad3b435b51404ee")), + ]: + result = raw_lmhash(secret, hex=True) + self.assertEqual(result, hash) + + def test_nthash(self): + warnings.filterwarnings("ignore", + r"nthash\.raw_nthash\(\) is deprecated") + from passlib.win32 import raw_nthash + for secret, hash in [ + ("OLDPASSWORD", u("6677b2c394311355b54f25eec5bfacf5")), + ("NEWPASSWORD", u("256781a62031289d3c2c98c14f1efc8c")), + ]: + result = raw_nthash(secret, hex=True) + self.assertEqual(result, hash) + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/tox_support.py b/ansible/lib/python3.11/site-packages/passlib/tests/tox_support.py new file mode 100644 index 000000000..43170bc40 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/tests/tox_support.py @@ -0,0 +1,83 @@ +"""passlib.tests.tox_support - helper script for tox tests""" +#============================================================================= +# init script env +#============================================================================= +import os, sys +root_dir = os.path.join(os.path.dirname(__file__), os.pardir, os.pardir) +sys.path.insert(0, root_dir) + +#============================================================================= +# imports +#============================================================================= +# core +import re +import logging; log = logging.getLogger(__name__) +# site +# pkg +from passlib.utils.compat import print_ +# local +__all__ = [ +] + +#============================================================================= +# main +#============================================================================= +TH_PATH = "passlib.tests.test_handlers" + +def do_hash_tests(*args): + """return list of hash algorithm tests that match regexes""" + if not args: + print(TH_PATH) + return + suffix = '' + args = list(args) + while True: + if args[0] == "--method": + suffix = '.' + args[1] + del args[:2] + else: + break + from passlib.tests import test_handlers + names = [TH_PATH + ":" + name + suffix for name in dir(test_handlers) + if not name.startswith("_") and any(re.match(arg,name) for arg in args)] + print_("\n".join(names)) + return not names + +def do_preset_tests(name): + """return list of preset test names""" + if name == "django" or name == "django-hashes": + do_hash_tests("django_.*_test", "hex_md5_test") + if name == "django": + print_("passlib.tests.test_ext_django") + else: + raise ValueError("unknown name: %r" % name) + +def do_setup_gae(path, runtime): + """write fake GAE ``app.yaml`` to current directory so nosegae will work""" + from passlib.tests.utils import set_file + set_file(os.path.join(path, "app.yaml"), """\ +application: fake-app +version: 2 +runtime: %s +api_version: 1 +threadsafe: no + +handlers: +- url: /.* + script: dummy.py + +libraries: +- name: django + version: "latest" +""" % runtime) + +def main(cmd, *args): + return globals()["do_" + cmd](*args) + +if __name__ == "__main__": + import sys + sys.exit(main(*sys.argv[1:]) or 0) + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/utils.py b/ansible/lib/python3.11/site-packages/passlib/tests/utils.py new file mode 100644 index 000000000..79a9f9fc4 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/tests/utils.py @@ -0,0 +1,3621 @@ +"""helpers for passlib unittests""" +#============================================================================= +# imports +#============================================================================= +from __future__ import with_statement +# core +from binascii import unhexlify +import contextlib +from functools import wraps, partial +import hashlib +import logging; log = logging.getLogger(__name__) +import random +import re +import os +import sys +import tempfile +import threading +import time +from passlib.exc import PasslibHashWarning, PasslibConfigWarning +from passlib.utils.compat import PY3, JYTHON +import warnings +from warnings import warn +# site +# pkg +from passlib import exc +from passlib.exc import MissingBackendError +import passlib.registry as registry +from passlib.tests.backports import TestCase as _TestCase, skip, skipIf, skipUnless, SkipTest +from passlib.utils import has_rounds_info, has_salt_info, rounds_cost_values, \ + rng as sys_rng, getrandstr, is_ascii_safe, to_native_str, \ + repeat_string, tick, batch +from passlib.utils.compat import iteritems, irange, u, unicode, PY2, nullcontext +from passlib.utils.decor import classproperty +import passlib.utils.handlers as uh +# local +__all__ = [ + # util funcs + 'TEST_MODE', + 'set_file', 'get_file', + + # unit testing + 'TestCase', + 'HandlerCase', +] + +#============================================================================= +# environment detection +#============================================================================= +# figure out if we're running under GAE; +# some tests (e.g. FS writing) should be skipped. +# XXX: is there better way to do this? +try: + import google.appengine +except ImportError: + GAE = False +else: + GAE = True + +def ensure_mtime_changed(path): + """ensure file's mtime has changed""" + # NOTE: this is hack to deal w/ filesystems whose mtime resolution is >= 1s, + # when a test needs to be sure the mtime changed after writing to the file. + last = os.path.getmtime(path) + while os.path.getmtime(path) == last: + time.sleep(0.1) + os.utime(path, None) + +def _get_timer_resolution(timer): + def sample(): + start = cur = timer() + while start == cur: + cur = timer() + return cur-start + return min(sample() for _ in range(3)) +TICK_RESOLUTION = _get_timer_resolution(tick) + +#============================================================================= +# test mode +#============================================================================= +_TEST_MODES = ["quick", "default", "full"] +_test_mode = _TEST_MODES.index(os.environ.get("PASSLIB_TEST_MODE", + "default").strip().lower()) + +def TEST_MODE(min=None, max=None): + """check if test for specified mode should be enabled. + + ``"quick"`` + run the bare minimum tests to ensure functionality. + variable-cost hashes are tested at their lowest setting. + hash algorithms are only tested against the backend that will + be used on the current host. no fuzz testing is done. + + ``"default"`` + same as ``"quick"``, except: hash algorithms are tested + at default levels, and a brief round of fuzz testing is done + for each hash. + + ``"full"`` + extra regression and internal tests are enabled, hash algorithms are tested + against all available backends, unavailable ones are mocked whre possible, + additional time is devoted to fuzz testing. + """ + if min and _test_mode < _TEST_MODES.index(min): + return False + if max and _test_mode > _TEST_MODES.index(max): + return False + return True + +#============================================================================= +# hash object inspection +#============================================================================= +def has_relaxed_setting(handler): + """check if handler supports 'relaxed' kwd""" + # FIXME: I've been lazy, should probably just add 'relaxed' kwd + # to all handlers that derive from GenericHandler + + # ignore wrapper classes for now.. though could introspec. + if hasattr(handler, "orig_prefix"): + return False + + return 'relaxed' in handler.setting_kwds or issubclass(handler, + uh.GenericHandler) + +def get_effective_rounds(handler, rounds=None): + """get effective rounds value from handler""" + handler = unwrap_handler(handler) + return handler(rounds=rounds, use_defaults=True).rounds + +def is_default_backend(handler, backend): + """check if backend is the default for source""" + try: + orig = handler.get_backend() + except MissingBackendError: + return False + try: + handler.set_backend("default") + return handler.get_backend() == backend + finally: + handler.set_backend(orig) + +def iter_alt_backends(handler, current=None, fallback=False): + """ + iterate over alternate backends available to handler. + + .. warning:: + not thread-safe due to has_backend() call + """ + if current is None: + current = handler.get_backend() + backends = handler.backends + idx = backends.index(current)+1 if fallback else 0 + for backend in backends[idx:]: + if backend != current and handler.has_backend(backend): + yield backend + +def get_alt_backend(*args, **kwds): + for backend in iter_alt_backends(*args, **kwds): + return backend + return None + +def unwrap_handler(handler): + """return original handler, removing any wrapper objects""" + while hasattr(handler, "wrapped"): + handler = handler.wrapped + return handler + +def handler_derived_from(handler, base): + """ + test if was derived from via . + """ + # XXX: need way to do this more formally via ifc, + # for now just hacking in the cases we encounter in testing. + if handler == base: + return True + elif isinstance(handler, uh.PrefixWrapper): + while handler: + if handler == base: + return True + # helper set by PrefixWrapper().using() just for this case... + handler = handler._derived_from + return False + elif isinstance(handler, type) and issubclass(handler, uh.MinimalHandler): + return issubclass(handler, base) + else: + raise NotImplementedError("don't know how to inspect handler: %r" % (handler,)) + +@contextlib.contextmanager +def patch_calc_min_rounds(handler): + """ + internal helper for do_config_encrypt() -- + context manager which temporarily replaces handler's _calc_checksum() + with one that uses min_rounds; useful when trying to generate config + with high rounds value, but don't care if output is correct. + """ + if isinstance(handler, type) and issubclass(handler, uh.HasRounds): + # XXX: also require GenericHandler for this branch? + wrapped = handler._calc_checksum + def wrapper(self, *args, **kwds): + rounds = self.rounds + try: + self.rounds = self.min_rounds + return wrapped(self, *args, **kwds) + finally: + self.rounds = rounds + handler._calc_checksum = wrapper + try: + yield + finally: + handler._calc_checksum = wrapped + elif isinstance(handler, uh.PrefixWrapper): + with patch_calc_min_rounds(handler.wrapped): + yield + else: + yield + return + +#============================================================================= +# misc helpers +#============================================================================= +def set_file(path, content): + """set file to specified bytes""" + if isinstance(content, unicode): + content = content.encode("utf-8") + with open(path, "wb") as fh: + fh.write(content) + +def get_file(path): + """read file as bytes""" + with open(path, "rb") as fh: + return fh.read() + +def tonn(source): + """convert native string to non-native string""" + if not isinstance(source, str): + return source + elif PY3: + return source.encode("utf-8") + else: + try: + return source.decode("utf-8") + except UnicodeDecodeError: + return source.decode("latin-1") + +def hb(source): + """ + helper for represent byte strings in hex. + + usage: ``hb("deadbeef23")`` + """ + return unhexlify(re.sub(r"\s", "", source)) + +def limit(value, lower, upper): + if value < lower: + return lower + elif value > upper: + return upper + return value + +def quicksleep(delay): + """because time.sleep() doesn't even have 10ms accuracy on some OSes""" + start = tick() + while tick()-start < delay: + pass + +def time_call(func, setup=None, maxtime=1, bestof=10): + """ + timeit() wrapper which tries to get as accurate a measurement as possible w/in maxtime seconds. + + :returns: + ``(avg_seconds_per_call, log10_number_of_repetitions)`` + """ + from timeit import Timer + from math import log + timer = Timer(func, setup=setup or '') + number = 1 + end = tick() + maxtime + while True: + delta = min(timer.repeat(bestof, number)) + if tick() >= end: + return delta/number, int(log(number, 10)) + number *= 10 + +def run_with_fixed_seeds(count=128, master_seed=0x243F6A8885A308D3): + """ + decorator run test method w/ multiple fixed seeds. + """ + def builder(func): + @wraps(func) + def wrapper(*args, **kwds): + rng = random.Random(master_seed) + for _ in irange(count): + kwds['seed'] = rng.getrandbits(32) + func(*args, **kwds) + return wrapper + return builder + +#============================================================================= +# custom test harness +#============================================================================= + +class TestCase(_TestCase): + """passlib-specific test case class + + this class adds a number of features to the standard TestCase... + * common prefix for all test descriptions + * resets warnings filter & registry for every test + * tweaks to message formatting + * __msg__ kwd added to assertRaises() + * suite of methods for matching against warnings + """ + #=================================================================== + # add various custom features + #=================================================================== + + #--------------------------------------------------------------- + # make it easy for test cases to add common prefix to shortDescription + #--------------------------------------------------------------- + + # string prepended to all tests in TestCase + descriptionPrefix = None + + def shortDescription(self): + """wrap shortDescription() method to prepend descriptionPrefix""" + desc = super(TestCase, self).shortDescription() + prefix = self.descriptionPrefix + if prefix: + desc = "%s: %s" % (prefix, desc or str(self)) + return desc + + #--------------------------------------------------------------- + # hack things so nose and ut2 both skip subclasses who have + # "__unittest_skip=True" set, or whose names start with "_" + #--------------------------------------------------------------- + @classproperty + def __unittest_skip__(cls): + # NOTE: this attr is technically a unittest2 internal detail. + name = cls.__name__ + return name.startswith("_") or \ + getattr(cls, "_%s__unittest_skip" % name, False) + + @classproperty + def __test__(cls): + # make nose just proxy __unittest_skip__ + return not cls.__unittest_skip__ + + # flag to skip *this* class + __unittest_skip = True + + #--------------------------------------------------------------- + # reset warning filters & registry before each test + #--------------------------------------------------------------- + + # flag to reset all warning filters & ignore state + resetWarningState = True + + def setUp(self): + super(TestCase, self).setUp() + self.setUpWarnings() + # have uh.debug_only_repr() return real values for duration of test + self.patchAttr(exc, "ENABLE_DEBUG_ONLY_REPR", True) + + def setUpWarnings(self): + """helper to init warning filters before subclass setUp()""" + if self.resetWarningState: + ctx = reset_warnings() + ctx.__enter__() + self.addCleanup(ctx.__exit__) + + # ignore security warnings, tests may deliberately cause these + # TODO: may want to filter out a few of this, but not blanket filter... + # warnings.filterwarnings("ignore", category=exc.PasslibSecurityWarning) + + # ignore warnings about PasswordHash features deprecated in 1.7 + # TODO: should be cleaned in 2.0, when support will be dropped. + # should be kept until then, so we test the legacy paths. + warnings.filterwarnings("ignore", r"the method .*\.(encrypt|genconfig|genhash)\(\) is deprecated") + warnings.filterwarnings("ignore", r"the 'vary_rounds' option is deprecated") + warnings.filterwarnings("ignore", r"Support for `(py-bcrypt|bcryptor)` is deprecated") + + #--------------------------------------------------------------- + # tweak message formatting so longMessage mode is only enabled + # if msg ends with ":", and turn on longMessage by default. + #--------------------------------------------------------------- + longMessage = True + + def _formatMessage(self, msg, std): + if self.longMessage and msg and msg.rstrip().endswith(":"): + return '%s %s' % (msg.rstrip(), std) + else: + return msg or std + + #--------------------------------------------------------------- + # override assertRaises() to support '__msg__' keyword, + # and to return the caught exception for further examination + #--------------------------------------------------------------- + def assertRaises(self, _exc_type, _callable=None, *args, **kwds): + msg = kwds.pop("__msg__", None) + if _callable is None: + # FIXME: this ignores 'msg' + return super(TestCase, self).assertRaises(_exc_type, None, + *args, **kwds) + try: + result = _callable(*args, **kwds) + except _exc_type as err: + return err + std = "function returned %r, expected it to raise %r" % (result, + _exc_type) + raise self.failureException(self._formatMessage(msg, std)) + + #--------------------------------------------------------------- + # forbid a bunch of deprecated aliases so I stop using them + #--------------------------------------------------------------- + def assertEquals(self, *a, **k): + raise AssertionError("this alias is deprecated by unittest2") + assertNotEquals = assertRegexMatches = assertEquals + + #=================================================================== + # custom methods for matching warnings + #=================================================================== + def assertWarning(self, warning, + message_re=None, message=None, + category=None, + filename_re=None, filename=None, + lineno=None, + msg=None, + ): + """check if warning matches specified parameters. + 'warning' is the instance of Warning to match against; + can also be instance of WarningMessage (as returned by catch_warnings). + """ + # check input type + if hasattr(warning, "category"): + # resolve WarningMessage -> Warning, but preserve original + wmsg = warning + warning = warning.message + else: + # no original WarningMessage, passed raw Warning + wmsg = None + + # tests that can use a warning instance or WarningMessage object + if message: + self.assertEqual(str(warning), message, msg) + if message_re: + self.assertRegex(str(warning), message_re, msg) + if category: + self.assertIsInstance(warning, category, msg) + + # tests that require a WarningMessage object + if filename or filename_re: + if not wmsg: + raise TypeError("matching on filename requires a " + "WarningMessage instance") + real = wmsg.filename + if real.endswith(".pyc") or real.endswith(".pyo"): + # FIXME: should use a stdlib call to resolve this back + # to module's original filename. + real = real[:-1] + if filename: + self.assertEqual(real, filename, msg) + if filename_re: + self.assertRegex(real, filename_re, msg) + if lineno: + if not wmsg: + raise TypeError("matching on lineno requires a " + "WarningMessage instance") + self.assertEqual(wmsg.lineno, lineno, msg) + + class _AssertWarningList(warnings.catch_warnings): + """context manager for assertWarningList()""" + def __init__(self, case, **kwds): + self.case = case + self.kwds = kwds + self.__super = super(TestCase._AssertWarningList, self) + self.__super.__init__(record=True) + + def __enter__(self): + self.log = self.__super.__enter__() + + def __exit__(self, *exc_info): + self.__super.__exit__(*exc_info) + if exc_info[0] is None: + self.case.assertWarningList(self.log, **self.kwds) + + def assertWarningList(self, wlist=None, desc=None, msg=None): + """check that warning list (e.g. from catch_warnings) matches pattern""" + if desc is None: + assert wlist is not None + return self._AssertWarningList(self, desc=wlist, msg=msg) + # TODO: make this display better diff of *which* warnings did not match + assert desc is not None + if not isinstance(desc, (list,tuple)): + desc = [desc] + for idx, entry in enumerate(desc): + if isinstance(entry, str): + entry = dict(message_re=entry) + elif isinstance(entry, type) and issubclass(entry, Warning): + entry = dict(category=entry) + elif not isinstance(entry, dict): + raise TypeError("entry must be str, warning, or dict") + try: + data = wlist[idx] + except IndexError: + break + self.assertWarning(data, msg=msg, **entry) + else: + if len(wlist) == len(desc): + return + std = "expected %d warnings, found %d: wlist=%s desc=%r" % \ + (len(desc), len(wlist), self._formatWarningList(wlist), desc) + raise self.failureException(self._formatMessage(msg, std)) + + def consumeWarningList(self, wlist, desc=None, *args, **kwds): + """[deprecated] assertWarningList() variant that clears list afterwards""" + if desc is None: + desc = [] + self.assertWarningList(wlist, desc, *args, **kwds) + del wlist[:] + + def _formatWarning(self, entry): + tail = "" + if hasattr(entry, "message"): + # WarningMessage instance. + tail = " filename=%r lineno=%r" % (entry.filename, entry.lineno) + if entry.line: + tail += " line=%r" % (entry.line,) + entry = entry.message + cls = type(entry) + return "<%s.%s message=%r%s>" % (cls.__module__, cls.__name__, + str(entry), tail) + + def _formatWarningList(self, wlist): + return "[%s]" % ", ".join(self._formatWarning(entry) for entry in wlist) + + #=================================================================== + # capability tests + #=================================================================== + def require_stringprep(self): + """helper to skip test if stringprep is missing""" + from passlib.utils import stringprep + if not stringprep: + from passlib.utils import _stringprep_missing_reason + raise self.skipTest("not available - stringprep module is " + + _stringprep_missing_reason) + + def require_TEST_MODE(self, level): + """skip test for all PASSLIB_TEST_MODE values below """ + if not TEST_MODE(level): + raise self.skipTest("requires >= %r test mode" % level) + + def require_writeable_filesystem(self): + """skip test if writeable FS not available""" + if GAE: + return self.skipTest("GAE doesn't offer read/write filesystem access") + + #=================================================================== + # reproducible random helpers + #=================================================================== + + #: global thread lock for random state + #: XXX: could split into global & per-instance locks if need be + _random_global_lock = threading.Lock() + + #: cache of global seed value, initialized on first call to getRandom() + _random_global_seed = None + + #: per-instance cache of name -> RNG + _random_cache = None + + def getRandom(self, name="default", seed=None): + """ + Return a :class:`random.Random` object for current test method to use. + Within an instance, multiple calls with the same name will return + the same object. + + When first created, each RNG will be seeded with value derived from + a global seed, the test class module & name, the current test method name, + and the **name** parameter. + + The global seed taken from the $RANDOM_TEST_SEED env var, + the $PYTHONHASHSEED env var, or a randomly generated the + first time this method is called. In all cases, the value + is logged for reproducibility. + + :param name: + name to uniquely identify separate RNGs w/in a test + (e.g. for threaded tests). + + :param seed: + override global seed when initialzing rng. + + :rtype: random.Random + """ + # check cache + cache = self._random_cache + if cache and name in cache: + return cache[name] + + with self._random_global_lock: + + # check cache again, and initialize it + cache = self._random_cache + if cache and name in cache: + return cache[name] + elif not cache: + cache = self._random_cache = {} + + # init global seed + global_seed = seed or TestCase._random_global_seed + if global_seed is None: + # NOTE: checking PYTHONHASHSEED, because if that's set, + # the test runner wants something reproducible. + global_seed = TestCase._random_global_seed = \ + int(os.environ.get("RANDOM_TEST_SEED") or + os.environ.get("PYTHONHASHSEED") or + sys_rng.getrandbits(32)) + # XXX: would it be better to print() this? + log.info("using RANDOM_TEST_SEED=%d", global_seed) + + # create seed + cls = type(self) + source = "\n".join([str(global_seed), cls.__module__, cls.__name__, + self._testMethodName, name]) + digest = hashlib.sha256(source.encode("utf-8")).hexdigest() + seed = int(digest[:16], 16) + + # create rng + value = cache[name] = random.Random(seed) + return value + + #=================================================================== + # subtests + #=================================================================== + + has_real_subtest = hasattr(_TestCase, "subTest") + + @contextlib.contextmanager + def subTest(self, *args, **kwds): + """ + wrapper/backport for .subTest() which also traps SkipTest errors. + (see source for details) + """ + # this function works around two things: + # * TestCase.subTest() wasn't added until Py34; so for older python versions, + # we either need unittest2 installed, or provide stub of our own. + # this method provides a stub if needed (based on .has_real_subtest check) + # + # * as 2020-10-08, .subTest() doesn't play nicely w/ .skipTest(); + # and also makes it hard to debug which subtest had a failure. + # (see https://bugs.python.org/issue25894 and https://bugs.python.org/issue35327) + # this method traps skipTest exceptions, and adds some logging to help debug + # which subtest caused the issue. + + # setup way to log subtest info + # XXX: would like better way to inject messages into test output; + # but this at least gets us something for debugging... + # NOTE: this hack will miss parent params if called from nested .subTest() + def _render_title(_msg=None, **params): + out = ("[%s] " % _msg if _msg else "") + if params: + out += "(%s)" % " ".join("%s=%r" % tuple(item) for item in params.items()) + return out.strip() or "" + + test_log = self.getLogger() + title = _render_title(*args, **kwds) + + # use real subtest manager if available + if self.has_real_subtest: + ctx = super(TestCase, self).subTest(*args, **kwds) + else: + ctx = nullcontext() + + # run the subtest + with ctx: + test_log.info("running subtest: %s", title) + try: + yield + except SkipTest: + # silence "SkipTest" exceptions, want to keep running next subtest. + test_log.info("subtest skipped: %s", title) + pass + except Exception as err: + # log unhandled exception occurred + # (assuming traceback will be reported up higher, so not bothering here) + test_log.warning("subtest failed: %s: %s: %r", title, type(err).__name__, str(err)) + raise + + # XXX: check for "failed" state in ``self._outcome`` before writing this? + test_log.info("subtest passed: %s", title) + + #=================================================================== + # other + #=================================================================== + _mktemp_queue = None + + def mktemp(self, *args, **kwds): + """create temp file that's cleaned up at end of test""" + self.require_writeable_filesystem() + fd, path = tempfile.mkstemp(*args, **kwds) + os.close(fd) + queue = self._mktemp_queue + if queue is None: + queue = self._mktemp_queue = [] + def cleaner(): + for path in queue: + if os.path.exists(path): + os.remove(path) + del queue[:] + self.addCleanup(cleaner) + queue.append(path) + return path + + def patchAttr(self, obj, attr, value, require_existing=True, wrap=False): + """monkeypatch object value, restoring original value on cleanup""" + try: + orig = getattr(obj, attr) + except AttributeError: + if require_existing: + raise + def cleanup(): + try: + delattr(obj, attr) + except AttributeError: + pass + self.addCleanup(cleanup) + else: + self.addCleanup(setattr, obj, attr, orig) + if wrap: + value = partial(value, orig) + wraps(orig)(value) + setattr(obj, attr, value) + + def getLogger(self): + """ + return logger named after current test. + """ + cls = type(self) + # NOTE: conditional on qualname for PY2 compat + path = cls.__module__ + "." + getattr(cls, "__qualname__", cls.__name__) + name = self._testMethodName + if name: + path = path + "." + name + return logging.getLogger(path) + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# other unittest helpers +#============================================================================= + +RESERVED_BACKEND_NAMES = ["any", "default"] + + +def doesnt_require_backend(func): + """ + decorator for HandlerCase.create_backend_case() -- + used to decorate methods that should be run even if backend isn't present + (by default, full test suite is skipped when backend is missing) + + NOTE: tests decorated with this should not rely on handler have expected (or any!) backend. + """ + func._doesnt_require_backend = True + return func + + +class HandlerCase(TestCase): + """base class for testing password hash handlers (esp passlib.utils.handlers subclasses) + + In order to use this to test a handler, + create a subclass will all the appropriate attributes + filled as listed in the example below, + and run the subclass via unittest. + + .. todo:: + + Document all of the options HandlerCase offers. + + .. note:: + + This is subclass of :class:`unittest.TestCase` + (or :class:`unittest2.TestCase` if available). + """ + #=================================================================== + # class attrs - should be filled in by subclass + #=================================================================== + + #--------------------------------------------------------------- + # handler setup + #--------------------------------------------------------------- + + # handler class to test [required] + handler = None + + # if set, run tests against specified backend + backend = None + + #--------------------------------------------------------------- + # test vectors + #--------------------------------------------------------------- + + # list of (secret, hash) tuples which are known to be correct + known_correct_hashes = [] + + # list of (config, secret, hash) tuples are known to be correct + known_correct_configs = [] + + # list of (alt_hash, secret, hash) tuples, where alt_hash is a hash + # using an alternate representation that should be recognized and verify + # correctly, but should be corrected to match hash when passed through + # genhash() + known_alternate_hashes = [] + + # hashes so malformed they aren't even identified properly + known_unidentified_hashes = [] + + # hashes which are identifiabled but malformed - they should identify() + # as True, but cause an error when passed to genhash/verify. + known_malformed_hashes = [] + + # list of (handler name, hash) pairs for other algorithm's hashes that + # handler shouldn't identify as belonging to it this list should generally + # be sufficient (if handler name in list, that entry will be skipped) + known_other_hashes = [ + ('des_crypt', '6f8c114b58f2c'), + ('md5_crypt', '$1$dOHYPKoP$tnxS1T8Q6VVn3kpV8cN6o.'), + ('sha512_crypt', "$6$rounds=123456$asaltof16chars..$BtCwjqMJGx5hrJhZywW" + "vt0RLE8uZ4oPwcelCjmw2kSYu.Ec6ycULevoBK25fs2xXgMNrCzIMVcgEJAstJeonj1"), + ] + + # passwords used to test basic hash behavior - generally + # don't need to be overidden. + stock_passwords = [ + u("test"), + u("\u20AC\u00A5$"), + b'\xe2\x82\xac\xc2\xa5$' + ] + + #--------------------------------------------------------------- + # option flags + #--------------------------------------------------------------- + + # whether hash is case insensitive + # True, False, or special value "verify-only" (which indicates + # hash contains case-sensitive portion, but verifies is case-insensitive) + secret_case_insensitive = False + + # flag if scheme accepts ALL hash strings (e.g. plaintext) + accepts_all_hashes = False + + # flag if scheme has "is_disabled" set, and contains 'salted' data + disabled_contains_salt = False + + # flag/hack to filter PasslibHashWarning issued by test_72_configs() + filter_config_warnings = False + + # forbid certain characters in passwords + @classproperty + def forbidden_characters(cls): + # anything that supports crypt() interface should forbid null chars, + # since crypt() uses null-terminated strings. + if 'os_crypt' in getattr(cls.handler, "backends", ()): + return b"\x00" + return None + + #=================================================================== + # internal class attrs + #=================================================================== + __unittest_skip = True + + @property + def descriptionPrefix(self): + handler = self.handler + name = handler.name + if hasattr(handler, "get_backend"): + name += " (%s backend)" % (handler.get_backend(),) + return name + + #=================================================================== + # support methods + #=================================================================== + + #--------------------------------------------------------------- + # configuration helpers + #--------------------------------------------------------------- + @classmethod + def iter_known_hashes(cls): + """iterate through known (secret, hash) pairs""" + for secret, hash in cls.known_correct_hashes: + yield secret, hash + for config, secret, hash in cls.known_correct_configs: + yield secret, hash + for alt, secret, hash in cls.known_alternate_hashes: + yield secret, hash + + def get_sample_hash(self): + """test random sample secret/hash pair""" + known = list(self.iter_known_hashes()) + return self.getRandom().choice(known) + + #--------------------------------------------------------------- + # test helpers + #--------------------------------------------------------------- + def check_verify(self, secret, hash, msg=None, negate=False): + """helper to check verify() outcome, honoring is_disabled_handler""" + result = self.do_verify(secret, hash) + self.assertTrue(result is True or result is False, + "verify() returned non-boolean value: %r" % (result,)) + if self.handler.is_disabled or negate: + if not result: + return + if not msg: + msg = ("verify incorrectly returned True: secret=%r, hash=%r" % + (secret, hash)) + raise self.failureException(msg) + else: + if result: + return + if not msg: + msg = "verify failed: secret=%r, hash=%r" % (secret, hash) + raise self.failureException(msg) + + def check_returned_native_str(self, result, func_name): + self.assertIsInstance(result, str, + "%s() failed to return native string: %r" % (func_name, result,)) + + #--------------------------------------------------------------- + # PasswordHash helpers - wraps all calls to PasswordHash api, + # so that subclasses can fill in defaults and account for other specialized behavior + #--------------------------------------------------------------- + def populate_settings(self, kwds): + """subclassable method to populate default settings""" + # use lower rounds settings for certain test modes + handler = self.handler + if 'rounds' in handler.setting_kwds and 'rounds' not in kwds: + mn = handler.min_rounds + df = handler.default_rounds + if TEST_MODE(max="quick"): + # use minimum rounds for quick mode + kwds['rounds'] = max(3, mn) + else: + # use default/16 otherwise + factor = 3 + if getattr(handler, "rounds_cost", None) == "log2": + df -= factor + else: + df //= (1<= 1") + + # check min_salt_size + if cls.min_salt_size < 0: + raise AssertionError("min_salt_chars must be >= 0") + if mx_set and cls.min_salt_size > cls.max_salt_size: + raise AssertionError("min_salt_chars must be <= max_salt_chars") + + # check default_salt_size + if cls.default_salt_size < cls.min_salt_size: + raise AssertionError("default_salt_size must be >= min_salt_size") + if mx_set and cls.default_salt_size > cls.max_salt_size: + raise AssertionError("default_salt_size must be <= max_salt_size") + + # check for 'salt_size' keyword + # NOTE: skipping warning if default salt size is already maxed out + # (might change that in future) + if 'salt_size' not in cls.setting_kwds and (not mx_set or cls.default_salt_size < cls.max_salt_size): + warn('%s: hash handler supports range of salt sizes, ' + 'but doesn\'t offer \'salt_size\' setting' % (cls.name,)) + + # check salt_chars & default_salt_chars + if cls.salt_chars: + if not cls.default_salt_chars: + raise AssertionError("default_salt_chars must not be empty") + for c in cls.default_salt_chars: + if c not in cls.salt_chars: + raise AssertionError("default_salt_chars must be subset of salt_chars: %r not in salt_chars" % (c,)) + else: + if not cls.default_salt_chars: + raise AssertionError("default_salt_chars MUST be specified if salt_chars is empty") + + @property + def salt_bits(self): + """calculate number of salt bits in hash""" + # XXX: replace this with bitsize() method? + handler = self.handler + assert has_salt_info(handler), "need explicit bit-size for " + handler.name + from math import log + # FIXME: this may be off for case-insensitive hashes, but that accounts + # for ~1 bit difference, which is good enough for test_11() + return int(handler.default_salt_size * + log(len(handler.default_salt_chars), 2)) + + def test_11_unique_salt(self): + """test hash() / genconfig() creates new salt each time""" + self.require_salt() + # odds of picking 'n' identical salts at random is '(.5**salt_bits)**n'. + # we want to pick the smallest N needed s.t. odds are <1/10**d, just + # to eliminate false-positives. which works out to n>3.33+d-salt_bits. + # for 1/1e12 odds, n=1 is sufficient for most hashes, but a few border cases (e.g. + # cisco_type7) have < 16 bits of salt, requiring more. + samples = max(1, 4 + 12 - self.salt_bits) + + def sampler(func): + value1 = func() + for _ in irange(samples): + value2 = func() + if value1 != value2: + return + raise self.failureException("failed to find different salt after " + "%d samples" % (samples,)) + sampler(self.do_genconfig) + sampler(lambda: self.do_encrypt("stub")) + + def test_12_min_salt_size(self): + """test hash() / genconfig() honors min_salt_size""" + self.require_salt_info() + + handler = self.handler + salt_char = handler.salt_chars[0:1] + min_size = handler.min_salt_size + + # + # check min is accepted + # + s1 = salt_char * min_size + self.do_genconfig(salt=s1) + + self.do_encrypt('stub', salt_size=min_size) + + # + # check min-1 is rejected + # + if min_size > 0: + self.assertRaises(ValueError, self.do_genconfig, + salt=s1[:-1]) + + self.assertRaises(ValueError, self.do_encrypt, 'stub', + salt_size=min_size-1) + + def test_13_max_salt_size(self): + """test hash() / genconfig() honors max_salt_size""" + self.require_salt_info() + + handler = self.handler + max_size = handler.max_salt_size + salt_char = handler.salt_chars[0:1] + + # NOTE: skipping this for hashes like argon2 since max_salt_size takes WAY too much memory + if max_size is None or max_size > (1 << 20): + # + # if it's not set, salt should never be truncated; so test it + # with an unreasonably large salt. + # + s1 = salt_char * 1024 + c1 = self.do_stub_encrypt(salt=s1) + c2 = self.do_stub_encrypt(salt=s1 + salt_char) + self.assertNotEqual(c1, c2) + + self.do_stub_encrypt(salt_size=1024) + + else: + # + # check max size is accepted + # + s1 = salt_char * max_size + c1 = self.do_stub_encrypt(salt=s1) + + self.do_stub_encrypt(salt_size=max_size) + + # + # check max size + 1 is rejected + # + s2 = s1 + salt_char + self.assertRaises(ValueError, self.do_stub_encrypt, salt=s2) + + self.assertRaises(ValueError, self.do_stub_encrypt, salt_size=max_size + 1) + + # + # should accept too-large salt in relaxed mode + # + if has_relaxed_setting(handler): + with warnings.catch_warnings(record=True): # issues passlibhandlerwarning + c2 = self.do_stub_encrypt(salt=s2, relaxed=True) + self.assertEqual(c2, c1) + + # + # if min_salt supports it, check smaller than mx is NOT truncated + # + if handler.min_salt_size < max_size: + c3 = self.do_stub_encrypt(salt=s1[:-1]) + self.assertNotEqual(c3, c1) + + # whether salt should be passed through bcrypt repair function + fuzz_salts_need_bcrypt_repair = False + + def prepare_salt(self, salt): + """prepare generated salt""" + if self.fuzz_salts_need_bcrypt_repair: + from passlib.utils.binary import bcrypt64 + salt = bcrypt64.repair_unused(salt) + return salt + + def test_14_salt_chars(self): + """test hash() honors salt_chars""" + self.require_salt_info() + + handler = self.handler + mx = handler.max_salt_size + mn = handler.min_salt_size + cs = handler.salt_chars + raw = isinstance(cs, bytes) + + # make sure all listed chars are accepted + for salt in batch(cs, mx or 32): + if len(salt) < mn: + salt = repeat_string(salt, mn) + salt = self.prepare_salt(salt) + self.do_stub_encrypt(salt=salt) + + # check some invalid salt chars, make sure they're rejected + source = u('\x00\xff') + if raw: + source = source.encode("latin-1") + chunk = max(mn, 1) + for c in source: + if c not in cs: + self.assertRaises(ValueError, self.do_stub_encrypt, salt=c*chunk, + __msg__="invalid salt char %r:" % (c,)) + + @property + def salt_type(self): + """hack to determine salt keyword's datatype""" + # NOTE: cisco_type7 uses 'int' + if getattr(self.handler, "_salt_is_bytes", False): + return bytes + else: + return unicode + + def test_15_salt_type(self): + """test non-string salt values""" + self.require_salt() + salt_type = self.salt_type + salt_size = getattr(self.handler, "min_salt_size", 0) or 8 + + # should always throw error for random class. + class fake(object): + pass + self.assertRaises(TypeError, self.do_encrypt, 'stub', salt=fake()) + + # unicode should be accepted only if salt_type is unicode. + if salt_type is not unicode: + self.assertRaises(TypeError, self.do_encrypt, 'stub', salt=u('x') * salt_size) + + # bytes should be accepted only if salt_type is bytes, + # OR if salt type is unicode and running PY2 - to allow native strings. + if not (salt_type is bytes or (PY2 and salt_type is unicode)): + self.assertRaises(TypeError, self.do_encrypt, 'stub', salt=b'x' * salt_size) + + def test_using_salt_size(self): + """Handler.using() -- default_salt_size""" + self.require_salt_info() + + handler = self.handler + mn = handler.min_salt_size + mx = handler.max_salt_size + df = handler.default_salt_size + + # should prevent setting below handler limit + self.assertRaises(ValueError, handler.using, default_salt_size=-1) + with self.assertWarningList([PasslibHashWarning]): + temp = handler.using(default_salt_size=-1, relaxed=True) + self.assertEqual(temp.default_salt_size, mn) + + # should prevent setting above handler limit + if mx: + self.assertRaises(ValueError, handler.using, default_salt_size=mx+1) + with self.assertWarningList([PasslibHashWarning]): + temp = handler.using(default_salt_size=mx+1, relaxed=True) + self.assertEqual(temp.default_salt_size, mx) + + # try setting to explicit value + if mn != mx: + temp = handler.using(default_salt_size=mn+1) + self.assertEqual(temp.default_salt_size, mn+1) + self.assertEqual(handler.default_salt_size, df) + + temp = handler.using(default_salt_size=mn+2) + self.assertEqual(temp.default_salt_size, mn+2) + self.assertEqual(handler.default_salt_size, df) + + # accept strings + if mn == mx: + ref = mn + else: + ref = mn + 1 + temp = handler.using(default_salt_size=str(ref)) + self.assertEqual(temp.default_salt_size, ref) + + # reject invalid strings + self.assertRaises(ValueError, handler.using, default_salt_size=str(ref) + "xxx") + + # honor 'salt_size' alias + temp = handler.using(salt_size=ref) + self.assertEqual(temp.default_salt_size, ref) + + #=================================================================== + # rounds + #=================================================================== + def require_rounds_info(self): + if not has_rounds_info(self.handler): + raise self.skipTest("handler lacks rounds attributes") + + def test_20_optional_rounds_attributes(self): + """validate optional rounds attributes""" + self.require_rounds_info() + + cls = self.handler + AssertionError = self.failureException + + # check max_rounds + if cls.max_rounds is None: + raise AssertionError("max_rounds not specified") + if cls.max_rounds < 1: + raise AssertionError("max_rounds must be >= 1") + + # check min_rounds + if cls.min_rounds < 0: + raise AssertionError("min_rounds must be >= 0") + if cls.min_rounds > cls.max_rounds: + raise AssertionError("min_rounds must be <= max_rounds") + + # check default_rounds + if cls.default_rounds is not None: + if cls.default_rounds < cls.min_rounds: + raise AssertionError("default_rounds must be >= min_rounds") + if cls.default_rounds > cls.max_rounds: + raise AssertionError("default_rounds must be <= max_rounds") + + # check rounds_cost + if cls.rounds_cost not in rounds_cost_values: + raise AssertionError("unknown rounds cost constant: %r" % (cls.rounds_cost,)) + + def test_21_min_rounds(self): + """test hash() / genconfig() honors min_rounds""" + self.require_rounds_info() + handler = self.handler + min_rounds = handler.min_rounds + + # check min is accepted + self.do_genconfig(rounds=min_rounds) + self.do_encrypt('stub', rounds=min_rounds) + + # check min-1 is rejected + self.assertRaises(ValueError, self.do_genconfig, rounds=min_rounds-1) + self.assertRaises(ValueError, self.do_encrypt, 'stub', rounds=min_rounds-1) + + # TODO: check relaxed mode clips min-1 + + def test_21b_max_rounds(self): + """test hash() / genconfig() honors max_rounds""" + self.require_rounds_info() + handler = self.handler + max_rounds = handler.max_rounds + + if max_rounds is not None: + # check max+1 is rejected + self.assertRaises(ValueError, self.do_genconfig, rounds=max_rounds+1) + self.assertRaises(ValueError, self.do_encrypt, 'stub', rounds=max_rounds+1) + + # handle max rounds + if max_rounds is None: + self.do_stub_encrypt(rounds=(1 << 31) - 1) + else: + self.do_stub_encrypt(rounds=max_rounds) + + # TODO: check relaxed mode clips max+1 + + #-------------------------------------------------------------------------------------- + # HasRounds.using() / .needs_update() -- desired rounds limits + #-------------------------------------------------------------------------------------- + def _create_using_rounds_helper(self): + """ + setup test helpers for testing handler.using()'s rounds parameters. + """ + self.require_rounds_info() + handler = self.handler + + if handler.name == "bsdi_crypt": + # hack to bypass bsdi-crypt's "odd rounds only" behavior, messes up this test + orig_handler = handler + handler = handler.using() + handler._generate_rounds = classmethod(lambda cls: super(orig_handler, cls)._generate_rounds()) + + # create some fake values to test with + orig_min_rounds = handler.min_rounds + orig_max_rounds = handler.max_rounds + orig_default_rounds = handler.default_rounds + medium = ((orig_max_rounds or 9999) + orig_min_rounds) // 2 + if medium == orig_default_rounds: + medium += 1 + small = (orig_min_rounds + medium) // 2 + large = ((orig_max_rounds or 9999) + medium) // 2 + + if handler.name == "bsdi_crypt": + # hack to avoid even numbered rounds + small |= 1 + medium |= 1 + large |= 1 + adj = 2 + else: + adj = 1 + + # create a subclass with small/medium/large as new default desired values + with self.assertWarningList([]): + subcls = handler.using( + min_desired_rounds=small, + max_desired_rounds=large, + default_rounds=medium, + ) + + # return helpers + return handler, subcls, small, medium, large, adj + + def test_has_rounds_using_harness(self): + """ + HasRounds.using() -- sanity check test harness + """ + # setup helpers + self.require_rounds_info() + handler = self.handler + orig_min_rounds = handler.min_rounds + orig_max_rounds = handler.max_rounds + orig_default_rounds = handler.default_rounds + handler, subcls, small, medium, large, adj = self._create_using_rounds_helper() + + # shouldn't affect original handler at all + self.assertEqual(handler.min_rounds, orig_min_rounds) + self.assertEqual(handler.max_rounds, orig_max_rounds) + self.assertEqual(handler.min_desired_rounds, None) + self.assertEqual(handler.max_desired_rounds, None) + self.assertEqual(handler.default_rounds, orig_default_rounds) + + # should affect subcls' desired value, but not hard min/max + self.assertEqual(subcls.min_rounds, orig_min_rounds) + self.assertEqual(subcls.max_rounds, orig_max_rounds) + self.assertEqual(subcls.default_rounds, medium) + self.assertEqual(subcls.min_desired_rounds, small) + self.assertEqual(subcls.max_desired_rounds, large) + + def test_has_rounds_using_w_min_rounds(self): + """ + HasRounds.using() -- min_rounds / min_desired_rounds + """ + # setup helpers + handler, subcls, small, medium, large, adj = self._create_using_rounds_helper() + orig_min_rounds = handler.min_rounds + orig_max_rounds = handler.max_rounds + orig_default_rounds = handler.default_rounds + + # .using() should clip values below valid minimum, w/ warning + if orig_min_rounds > 0: + self.assertRaises(ValueError, handler.using, min_desired_rounds=orig_min_rounds - adj) + with self.assertWarningList([PasslibHashWarning]): + temp = handler.using(min_desired_rounds=orig_min_rounds - adj, relaxed=True) + self.assertEqual(temp.min_desired_rounds, orig_min_rounds) + + # .using() should clip values above valid maximum, w/ warning + if orig_max_rounds: + self.assertRaises(ValueError, handler.using, min_desired_rounds=orig_max_rounds + adj) + with self.assertWarningList([PasslibHashWarning]): + temp = handler.using(min_desired_rounds=orig_max_rounds + adj, relaxed=True) + self.assertEqual(temp.min_desired_rounds, orig_max_rounds) + + # .using() should allow values below previous desired minimum, w/o warning + with self.assertWarningList([]): + temp = subcls.using(min_desired_rounds=small - adj) + self.assertEqual(temp.min_desired_rounds, small - adj) + + # .using() should allow values w/in previous range + temp = subcls.using(min_desired_rounds=small + 2 * adj) + self.assertEqual(temp.min_desired_rounds, small + 2 * adj) + + # .using() should allow values above previous desired maximum, w/o warning + with self.assertWarningList([]): + temp = subcls.using(min_desired_rounds=large + adj) + self.assertEqual(temp.min_desired_rounds, large + adj) + + # hash() etc should allow explicit values below desired minimum + # NOTE: formerly issued a warning in passlib 1.6, now just a wrapper for .using() + self.assertEqual(get_effective_rounds(subcls, small + adj), small + adj) + self.assertEqual(get_effective_rounds(subcls, small), small) + with self.assertWarningList([]): + self.assertEqual(get_effective_rounds(subcls, small - adj), small - adj) + + # 'min_rounds' should be treated as alias for 'min_desired_rounds' + temp = handler.using(min_rounds=small) + self.assertEqual(temp.min_desired_rounds, small) + + # should be able to specify strings + temp = handler.using(min_rounds=str(small)) + self.assertEqual(temp.min_desired_rounds, small) + + # invalid strings should cause error + self.assertRaises(ValueError, handler.using, min_rounds=str(small) + "xxx") + + def test_has_rounds_replace_w_max_rounds(self): + """ + HasRounds.using() -- max_rounds / max_desired_rounds + """ + # setup helpers + handler, subcls, small, medium, large, adj = self._create_using_rounds_helper() + orig_min_rounds = handler.min_rounds + orig_max_rounds = handler.max_rounds + + # .using() should clip values below valid minimum w/ warning + if orig_min_rounds > 0: + self.assertRaises(ValueError, handler.using, max_desired_rounds=orig_min_rounds - adj) + with self.assertWarningList([PasslibHashWarning]): + temp = handler.using(max_desired_rounds=orig_min_rounds - adj, relaxed=True) + self.assertEqual(temp.max_desired_rounds, orig_min_rounds) + + # .using() should clip values above valid maximum, w/ warning + if orig_max_rounds: + self.assertRaises(ValueError, handler.using, max_desired_rounds=orig_max_rounds + adj) + with self.assertWarningList([PasslibHashWarning]): + temp = handler.using(max_desired_rounds=orig_max_rounds + adj, relaxed=True) + self.assertEqual(temp.max_desired_rounds, orig_max_rounds) + + # .using() should clip values below previous minimum, w/ warning + with self.assertWarningList([PasslibConfigWarning]): + temp = subcls.using(max_desired_rounds=small - adj) + self.assertEqual(temp.max_desired_rounds, small) + + # .using() should reject explicit min > max + self.assertRaises(ValueError, subcls.using, + min_desired_rounds=medium+adj, + max_desired_rounds=medium-adj) + + # .using() should allow values w/in previous range + temp = subcls.using(min_desired_rounds=large - 2 * adj) + self.assertEqual(temp.min_desired_rounds, large - 2 * adj) + + # .using() should allow values above previous desired maximum, w/o warning + with self.assertWarningList([]): + temp = subcls.using(max_desired_rounds=large + adj) + self.assertEqual(temp.max_desired_rounds, large + adj) + + # hash() etc should allow explicit values above desired minimum, w/o warning + # NOTE: formerly issued a warning in passlib 1.6, now just a wrapper for .using() + self.assertEqual(get_effective_rounds(subcls, large - adj), large - adj) + self.assertEqual(get_effective_rounds(subcls, large), large) + with self.assertWarningList([]): + self.assertEqual(get_effective_rounds(subcls, large + adj), large + adj) + + # 'max_rounds' should be treated as alias for 'max_desired_rounds' + temp = handler.using(max_rounds=large) + self.assertEqual(temp.max_desired_rounds, large) + + # should be able to specify strings + temp = handler.using(max_desired_rounds=str(large)) + self.assertEqual(temp.max_desired_rounds, large) + + # invalid strings should cause error + self.assertRaises(ValueError, handler.using, max_desired_rounds=str(large) + "xxx") + + def test_has_rounds_using_w_default_rounds(self): + """ + HasRounds.using() -- default_rounds + """ + # setup helpers + handler, subcls, small, medium, large, adj = self._create_using_rounds_helper() + orig_max_rounds = handler.max_rounds + + # XXX: are there any other cases that need testing? + + # implicit default rounds -- increase to min_rounds + temp = subcls.using(min_rounds=medium+adj) + self.assertEqual(temp.default_rounds, medium+adj) + + # implicit default rounds -- decrease to max_rounds + temp = subcls.using(max_rounds=medium-adj) + self.assertEqual(temp.default_rounds, medium-adj) + + # explicit default rounds below desired minimum + # XXX: make this a warning if min is implicit? + self.assertRaises(ValueError, subcls.using, default_rounds=small-adj) + + # explicit default rounds above desired maximum + # XXX: make this a warning if max is implicit? + if orig_max_rounds: + self.assertRaises(ValueError, subcls.using, default_rounds=large+adj) + + # hash() etc should implicit default rounds, but get overridden + self.assertEqual(get_effective_rounds(subcls), medium) + self.assertEqual(get_effective_rounds(subcls, medium+adj), medium+adj) + + # should be able to specify strings + temp = handler.using(default_rounds=str(medium)) + self.assertEqual(temp.default_rounds, medium) + + # invalid strings should cause error + self.assertRaises(ValueError, handler.using, default_rounds=str(medium) + "xxx") + + def test_has_rounds_using_w_rounds(self): + """ + HasRounds.using() -- rounds + """ + # setup helpers + handler, subcls, small, medium, large, adj = self._create_using_rounds_helper() + orig_max_rounds = handler.max_rounds + + # 'rounds' should be treated as fallback for min, max, and default + temp = subcls.using(rounds=medium+adj) + self.assertEqual(temp.min_desired_rounds, medium+adj) + self.assertEqual(temp.default_rounds, medium+adj) + self.assertEqual(temp.max_desired_rounds, medium+adj) + + # 'rounds' should be treated as fallback for min, max, and default + temp = subcls.using(rounds=medium+1, min_rounds=small+adj, + default_rounds=medium, max_rounds=large-adj) + self.assertEqual(temp.min_desired_rounds, small+adj) + self.assertEqual(temp.default_rounds, medium) + self.assertEqual(temp.max_desired_rounds, large-adj) + + def test_has_rounds_using_w_vary_rounds_parsing(self): + """ + HasRounds.using() -- vary_rounds parsing + """ + # setup helpers + handler, subcls, small, medium, large, adj = self._create_using_rounds_helper() + + def parse(value): + return subcls.using(vary_rounds=value).vary_rounds + + # floats should be preserved + self.assertEqual(parse(0.1), 0.1) + self.assertEqual(parse('0.1'), 0.1) + + # 'xx%' should be converted to float + self.assertEqual(parse('10%'), 0.1) + + # ints should be preserved + self.assertEqual(parse(1000), 1000) + self.assertEqual(parse('1000'), 1000) + + # float bounds should be enforced + self.assertRaises(ValueError, parse, -0.1) + self.assertRaises(ValueError, parse, 1.1) + + def test_has_rounds_using_w_vary_rounds_generation(self): + """ + HasRounds.using() -- vary_rounds generation + """ + handler, subcls, small, medium, large, adj = self._create_using_rounds_helper() + + def get_effective_range(cls): + seen = set(get_effective_rounds(cls) for _ in irange(1000)) + return min(seen), max(seen) + + def assert_rounds_range(vary_rounds, lower, upper): + temp = subcls.using(vary_rounds=vary_rounds) + seen_lower, seen_upper = get_effective_range(temp) + self.assertEqual(seen_lower, lower, "vary_rounds had wrong lower limit:") + self.assertEqual(seen_upper, upper, "vary_rounds had wrong upper limit:") + + # test static + assert_rounds_range(0, medium, medium) + assert_rounds_range("0%", medium, medium) + + # test absolute + assert_rounds_range(adj, medium - adj, medium + adj) + assert_rounds_range(50, max(small, medium - 50), min(large, medium + 50)) + + # test relative - should shift over at 50% mark + if handler.rounds_cost == "log2": + # log rounds "50%" variance should only increase/decrease by 1 cost value + assert_rounds_range("1%", medium, medium) + assert_rounds_range("49%", medium, medium) + assert_rounds_range("50%", medium - adj, medium) + else: + # for linear rounds, range is frequently so huge, won't ever see ends. + # so we just check it's within an expected range. + lower, upper = get_effective_range(subcls.using(vary_rounds="50%")) + + self.assertGreaterEqual(lower, max(small, medium * 0.5)) + self.assertLessEqual(lower, max(small, medium * 0.8)) + + self.assertGreaterEqual(upper, min(large, medium * 1.2)) + self.assertLessEqual(upper, min(large, medium * 1.5)) + + def test_has_rounds_using_and_needs_update(self): + """ + HasRounds.using() -- desired_rounds + needs_update() + """ + handler, subcls, small, medium, large, adj = self._create_using_rounds_helper() + + temp = subcls.using(min_desired_rounds=small+2, max_desired_rounds=large-2) + + # generate some sample hashes + small_hash = self.do_stub_encrypt(subcls, rounds=small) + medium_hash = self.do_stub_encrypt(subcls, rounds=medium) + large_hash = self.do_stub_encrypt(subcls, rounds=large) + + # everything should be w/in bounds for original handler + self.assertFalse(subcls.needs_update(small_hash)) + self.assertFalse(subcls.needs_update(medium_hash)) + self.assertFalse(subcls.needs_update(large_hash)) + + # small & large should require update for temp handler + self.assertTrue(temp.needs_update(small_hash)) + self.assertFalse(temp.needs_update(medium_hash)) + self.assertTrue(temp.needs_update(large_hash)) + + #=================================================================== + # idents + #=================================================================== + def require_many_idents(self): + handler = self.handler + if not isinstance(handler, type) or not issubclass(handler, uh.HasManyIdents): + raise self.skipTest("handler doesn't derive from HasManyIdents") + + def test_30_HasManyIdents(self): + """validate HasManyIdents configuration""" + cls = self.handler + self.require_many_idents() + + # check settings + self.assertTrue('ident' in cls.setting_kwds) + + # check ident_values list + for value in cls.ident_values: + self.assertIsInstance(value, unicode, + "cls.ident_values must be unicode:") + self.assertTrue(len(cls.ident_values)>1, + "cls.ident_values must have 2+ elements:") + + # check default_ident value + self.assertIsInstance(cls.default_ident, unicode, + "cls.default_ident must be unicode:") + self.assertTrue(cls.default_ident in cls.ident_values, + "cls.default_ident must specify member of cls.ident_values") + + # check optional aliases list + if cls.ident_aliases: + for alias, ident in iteritems(cls.ident_aliases): + self.assertIsInstance(alias, unicode, + "cls.ident_aliases keys must be unicode:") # XXX: allow ints? + self.assertIsInstance(ident, unicode, + "cls.ident_aliases values must be unicode:") + self.assertTrue(ident in cls.ident_values, + "cls.ident_aliases must map to cls.ident_values members: %r" % (ident,)) + + # check constructor validates ident correctly. + handler = cls + hash = self.get_sample_hash()[1] + kwds = handler.parsehash(hash) + del kwds['ident'] + + # ... accepts good ident + handler(ident=cls.default_ident, **kwds) + + # ... requires ident w/o defaults + self.assertRaises(TypeError, handler, **kwds) + + # ... supplies default ident + handler(use_defaults=True, **kwds) + + # ... rejects bad ident + self.assertRaises(ValueError, handler, ident='xXx', **kwds) + + # TODO: check various supported idents + + def test_has_many_idents_using(self): + """HasManyIdents.using() -- 'default_ident' and 'ident' keywords""" + self.require_many_idents() + + # pick alt ident to test with + handler = self.handler + orig_ident = handler.default_ident + for alt_ident in handler.ident_values: + if alt_ident != orig_ident: + break + else: + raise AssertionError("expected to find alternate ident: default=%r values=%r" % + (orig_ident, handler.ident_values)) + + def effective_ident(cls): + cls = unwrap_handler(cls) + return cls(use_defaults=True).ident + + # keep default if nothing else specified + subcls = handler.using() + self.assertEqual(subcls.default_ident, orig_ident) + + # accepts alt ident + subcls = handler.using(default_ident=alt_ident) + self.assertEqual(subcls.default_ident, alt_ident) + self.assertEqual(handler.default_ident, orig_ident) + + # check subcls actually *generates* default ident, + # and that we didn't affect orig handler + self.assertEqual(effective_ident(subcls), alt_ident) + self.assertEqual(effective_ident(handler), orig_ident) + + # rejects bad ident + self.assertRaises(ValueError, handler.using, default_ident='xXx') + + # honor 'ident' alias + subcls = handler.using(ident=alt_ident) + self.assertEqual(subcls.default_ident, alt_ident) + self.assertEqual(handler.default_ident, orig_ident) + + # forbid both at same time + self.assertRaises(TypeError, handler.using, default_ident=alt_ident, ident=alt_ident) + + # check ident aliases are being honored + if handler.ident_aliases: + for alias, ident in handler.ident_aliases.items(): + subcls = handler.using(ident=alias) + self.assertEqual(subcls.default_ident, ident, msg="alias %r:" % alias) + + #=================================================================== + # password size limits + #=================================================================== + def test_truncate_error_setting(self): + """ + validate 'truncate_error' setting & related attributes + """ + # If it doesn't have truncate_size set, + # it shouldn't support truncate_error + hasher = self.handler + if hasher.truncate_size is None: + self.assertNotIn("truncate_error", hasher.setting_kwds) + return + + # if hasher defaults to silently truncating, + # it MUST NOT use .truncate_verify_reject, + # because resulting hashes wouldn't verify! + if not hasher.truncate_error: + self.assertFalse(hasher.truncate_verify_reject) + + # if hasher doesn't have configurable policy, + # it must throw error by default + if "truncate_error" not in hasher.setting_kwds: + self.assertTrue(hasher.truncate_error) + return + + # test value parsing + def parse_value(value): + return hasher.using(truncate_error=value).truncate_error + self.assertEqual(parse_value(None), hasher.truncate_error) + self.assertEqual(parse_value(True), True) + self.assertEqual(parse_value("true"), True) + self.assertEqual(parse_value(False), False) + self.assertEqual(parse_value("false"), False) + self.assertRaises(ValueError, parse_value, "xxx") + + def test_secret_wo_truncate_size(self): + """ + test no password size limits enforced (if truncate_size=None) + """ + # skip if hasher has a maximum password size + hasher = self.handler + if hasher.truncate_size is not None: + self.assertGreaterEqual(hasher.truncate_size, 1) + raise self.skipTest("truncate_size is set") + + # NOTE: this doesn't do an exhaustive search to verify algorithm + # doesn't have some cutoff point, it just tries + # 1024-character string, and alters the last char. + # as long as algorithm doesn't clip secret at point <1024, + # the new secret shouldn't verify. + + # hash a 1024-byte secret + secret = "too many secrets" * 16 + alt = "x" + hash = self.do_encrypt(secret) + + # check that verify doesn't silently reject secret + # (i.e. hasher mistakenly honors .truncate_verify_reject) + verify_success = not hasher.is_disabled + self.assertEqual(self.do_verify(secret, hash), verify_success, + msg="verify rejected correct secret") + + # alter last byte, should get different hash, which won't verify + alt_secret = secret[:-1] + alt + self.assertFalse(self.do_verify(alt_secret, hash), + "full password not used in digest") + + def test_secret_w_truncate_size(self): + """ + test password size limits raise truncate_error (if appropriate) + """ + #-------------------------------------------------- + # check if test is applicable + #-------------------------------------------------- + handler = self.handler + truncate_size = handler.truncate_size + if not truncate_size: + raise self.skipTest("truncate_size not set") + + #-------------------------------------------------- + # setup vars + #-------------------------------------------------- + # try to get versions w/ and w/o truncate_error set. + # set to None if policy isn't configruable + size_error_type = exc.PasswordSizeError + if "truncate_error" in handler.setting_kwds: + without_error = handler.using(truncate_error=False) + with_error = handler.using(truncate_error=True) + size_error_type = exc.PasswordTruncateError + elif handler.truncate_error: + without_error = None + with_error = handler + else: + # NOTE: this mode is currently an error in test_truncate_error_setting() + without_error = handler + with_error = None + + # create some test secrets + base = "too many secrets" + alt = "x" # char that's not in base, used to mutate test secrets + long_secret = repeat_string(base, truncate_size+1) + short_secret = long_secret[:-1] + alt_long_secret = long_secret[:-1] + alt + alt_short_secret = short_secret[:-1] + alt + + # init flags + short_verify_success = not handler.is_disabled + long_verify_success = short_verify_success and \ + not handler.truncate_verify_reject + + #-------------------------------------------------- + # do tests on length secret, and resulting hash. + # should pass regardless of truncate_error policy. + #-------------------------------------------------- + assert without_error or with_error + for cand_hasher in [without_error, with_error]: + + # create & hash string that's exactly chars. + short_hash = self.do_encrypt(short_secret, handler=cand_hasher) + + # check hash verifies, regardless of .truncate_verify_reject + self.assertEqual(self.do_verify(short_secret, short_hash, + handler=cand_hasher), + short_verify_success) + + # changing 'th char should invalidate hash + # if this fails, means (reported) truncate_size is too large. + self.assertFalse(self.do_verify(alt_short_secret, short_hash, + handler=with_error), + "truncate_size value is too large") + + # verify should truncate long secret before comparing + # (unless truncate_verify_reject is set) + self.assertEqual(self.do_verify(long_secret, short_hash, + handler=cand_hasher), + long_verify_success) + + #-------------------------------------------------- + # do tests on length secret, + # w/ truncate error disabled (should silently truncate) + #-------------------------------------------------- + if without_error: + + # create & hash string that's exactly truncate_size+1 chars + long_hash = self.do_encrypt(long_secret, handler=without_error) + + # check verifies against secret (unless truncate_verify_reject=True) + self.assertEqual(self.do_verify(long_secret, long_hash, + handler=without_error), + short_verify_success) + + # check mutating last char doesn't change outcome. + # if this fails, means (reported) truncate_size is too small. + self.assertEqual(self.do_verify(alt_long_secret, long_hash, + handler=without_error), + short_verify_success) + + # check short_secret verifies against this hash + # if this fails, means (reported) truncate_size is too large. + self.assertTrue(self.do_verify(short_secret, long_hash, + handler=without_error)) + + #-------------------------------------------------- + # do tests on length secret, + # w/ truncate error + #-------------------------------------------------- + if with_error: + + # with errors enabled, should forbid truncation. + err = self.assertRaises(size_error_type, self.do_encrypt, + long_secret, handler=with_error) + self.assertEqual(err.max_size, truncate_size) + + #=================================================================== + # password contents + #=================================================================== + def test_61_secret_case_sensitive(self): + """test password case sensitivity""" + hash_insensitive = self.secret_case_insensitive is True + verify_insensitive = self.secret_case_insensitive in [True, + "verify-only"] + + # test hashing lower-case verifies against lower & upper + lower = 'test' + upper = 'TEST' + h1 = self.do_encrypt(lower) + if verify_insensitive and not self.handler.is_disabled: + self.assertTrue(self.do_verify(upper, h1), + "verify() should not be case sensitive") + else: + self.assertFalse(self.do_verify(upper, h1), + "verify() should be case sensitive") + + # test hashing upper-case verifies against lower & upper + h2 = self.do_encrypt(upper) + if verify_insensitive and not self.handler.is_disabled: + self.assertTrue(self.do_verify(lower, h2), + "verify() should not be case sensitive") + else: + self.assertFalse(self.do_verify(lower, h2), + "verify() should be case sensitive") + + # test genhash + # XXX: 2.0: what about 'verify-only' hashes once genhash() is removed? + # won't have easy way to recreate w/ same config to see if hash differs. + # (though only hash this applies to is mssql2000) + h2 = self.do_genhash(upper, h1) + if hash_insensitive or (self.handler.is_disabled and not self.disabled_contains_salt): + self.assertEqual(h2, h1, + "genhash() should not be case sensitive") + else: + self.assertNotEqual(h2, h1, + "genhash() should be case sensitive") + + def test_62_secret_border(self): + """test non-string passwords are rejected""" + hash = self.get_sample_hash()[1] + + # secret=None + self.assertRaises(TypeError, self.do_encrypt, None) + self.assertRaises(TypeError, self.do_genhash, None, hash) + self.assertRaises(TypeError, self.do_verify, None, hash) + + # secret=int (picked as example of entirely wrong class) + self.assertRaises(TypeError, self.do_encrypt, 1) + self.assertRaises(TypeError, self.do_genhash, 1, hash) + self.assertRaises(TypeError, self.do_verify, 1, hash) + + # xxx: move to password size limits section, above? + def test_63_large_secret(self): + """test MAX_PASSWORD_SIZE is enforced""" + from passlib.exc import PasswordSizeError + from passlib.utils import MAX_PASSWORD_SIZE + secret = '.' * (1+MAX_PASSWORD_SIZE) + hash = self.get_sample_hash()[1] + err = self.assertRaises(PasswordSizeError, self.do_genhash, secret, hash) + self.assertEqual(err.max_size, MAX_PASSWORD_SIZE) + self.assertRaises(PasswordSizeError, self.do_encrypt, secret) + self.assertRaises(PasswordSizeError, self.do_verify, secret, hash) + + def test_64_forbidden_chars(self): + """test forbidden characters not allowed in password""" + chars = self.forbidden_characters + if not chars: + raise self.skipTest("none listed") + base = u('stub') + if isinstance(chars, bytes): + from passlib.utils.compat import iter_byte_chars + chars = iter_byte_chars(chars) + base = base.encode("ascii") + for c in chars: + self.assertRaises(ValueError, self.do_encrypt, base + c + base) + + #=================================================================== + # check identify(), verify(), genhash() against test vectors + #=================================================================== + def is_secret_8bit(self, secret): + secret = self.populate_context(secret, {}) + return not is_ascii_safe(secret) + + def expect_os_crypt_failure(self, secret): + """ + check if we're expecting potential verify failure due to crypt.crypt() encoding limitation + """ + if PY3 and self.backend == "os_crypt" and isinstance(secret, bytes): + try: + secret.decode("utf-8") + except UnicodeDecodeError: + return True + return False + + def test_70_hashes(self): + """test known hashes""" + + # sanity check + self.assertTrue(self.known_correct_hashes or self.known_correct_configs, + "test must set at least one of 'known_correct_hashes' " + "or 'known_correct_configs'") + + # run through known secret/hash pairs + saw8bit = False + for secret, hash in self.iter_known_hashes(): + if self.is_secret_8bit(secret): + saw8bit = True + + # hash should be positively identified by handler + self.assertTrue(self.do_identify(hash), + "identify() failed to identify hash: %r" % (hash,)) + + # check if what we're about to do is expected to fail due to crypt.crypt() limitation. + expect_os_crypt_failure = self.expect_os_crypt_failure(secret) + try: + + # secret should verify successfully against hash + self.check_verify(secret, hash, "verify() of known hash failed: " + "secret=%r, hash=%r" % (secret, hash)) + + # genhash() should reproduce same hash + result = self.do_genhash(secret, hash) + self.assertIsInstance(result, str, + "genhash() failed to return native string: %r" % (result,)) + if self.handler.is_disabled and self.disabled_contains_salt: + continue + self.assertEqual(result, hash, "genhash() failed to reproduce " + "known hash: secret=%r, hash=%r: result=%r" % + (secret, hash, result)) + + except MissingBackendError: + if not expect_os_crypt_failure: + raise + + # would really like all handlers to have at least one 8-bit test vector + if not saw8bit: + warn("%s: no 8-bit secrets tested" % self.__class__) + + def test_71_alternates(self): + """test known alternate hashes""" + if not self.known_alternate_hashes: + raise self.skipTest("no alternate hashes provided") + for alt, secret, hash in self.known_alternate_hashes: + + # hash should be positively identified by handler + self.assertTrue(self.do_identify(hash), + "identify() failed to identify alternate hash: %r" % + (hash,)) + + # secret should verify successfully against hash + self.check_verify(secret, alt, "verify() of known alternate hash " + "failed: secret=%r, hash=%r" % (secret, alt)) + + # genhash() should reproduce canonical hash + result = self.do_genhash(secret, alt) + self.assertIsInstance(result, str, + "genhash() failed to return native string: %r" % (result,)) + if self.handler.is_disabled and self.disabled_contains_salt: + continue + self.assertEqual(result, hash, "genhash() failed to normalize " + "known alternate hash: secret=%r, alt=%r, hash=%r: " + "result=%r" % (secret, alt, hash, result)) + + def test_72_configs(self): + """test known config strings""" + # special-case handlers without settings + if not self.handler.setting_kwds: + self.assertFalse(self.known_correct_configs, + "handler should not have config strings") + raise self.skipTest("hash has no settings") + + if not self.known_correct_configs: + # XXX: make this a requirement? + raise self.skipTest("no config strings provided") + + # make sure config strings work (hashes in list tested in test_70) + if self.filter_config_warnings: + warnings.filterwarnings("ignore", category=PasslibHashWarning) + for config, secret, hash in self.known_correct_configs: + + # config should be positively identified by handler + self.assertTrue(self.do_identify(config), + "identify() failed to identify known config string: %r" % + (config,)) + + # verify() should throw error for config strings. + self.assertRaises(ValueError, self.do_verify, secret, config, + __msg__="verify() failed to reject config string: %r" % + (config,)) + + # genhash() should reproduce hash from config. + result = self.do_genhash(secret, config) + self.assertIsInstance(result, str, + "genhash() failed to return native string: %r" % (result,)) + self.assertEqual(result, hash, "genhash() failed to reproduce " + "known hash from config: secret=%r, config=%r, hash=%r: " + "result=%r" % (secret, config, hash, result)) + + def test_73_unidentified(self): + """test known unidentifiably-mangled strings""" + if not self.known_unidentified_hashes: + raise self.skipTest("no unidentified hashes provided") + for hash in self.known_unidentified_hashes: + + # identify() should reject these + self.assertFalse(self.do_identify(hash), + "identify() incorrectly identified known unidentifiable " + "hash: %r" % (hash,)) + + # verify() should throw error + self.assertRaises(ValueError, self.do_verify, 'stub', hash, + __msg__= "verify() failed to throw error for unidentifiable " + "hash: %r" % (hash,)) + + # genhash() should throw error + self.assertRaises(ValueError, self.do_genhash, 'stub', hash, + __msg__= "genhash() failed to throw error for unidentifiable " + "hash: %r" % (hash,)) + + def test_74_malformed(self): + """test known identifiable-but-malformed strings""" + if not self.known_malformed_hashes: + raise self.skipTest("no malformed hashes provided") + for hash in self.known_malformed_hashes: + + # identify() should accept these + self.assertTrue(self.do_identify(hash), + "identify() failed to identify known malformed " + "hash: %r" % (hash,)) + + # verify() should throw error + self.assertRaises(ValueError, self.do_verify, 'stub', hash, + __msg__= "verify() failed to throw error for malformed " + "hash: %r" % (hash,)) + + # genhash() should throw error + self.assertRaises(ValueError, self.do_genhash, 'stub', hash, + __msg__= "genhash() failed to throw error for malformed " + "hash: %r" % (hash,)) + + def test_75_foreign(self): + """test known foreign hashes""" + if self.accepts_all_hashes: + raise self.skipTest("not applicable") + if not self.known_other_hashes: + raise self.skipTest("no foreign hashes provided") + for name, hash in self.known_other_hashes: + # NOTE: most tests use default list of foreign hashes, + # so they may include ones belonging to that hash... + # hence the 'own' logic. + + if name == self.handler.name: + # identify should accept these + self.assertTrue(self.do_identify(hash), + "identify() failed to identify known hash: %r" % (hash,)) + + # verify & genhash should NOT throw error + self.do_verify('stub', hash) + result = self.do_genhash('stub', hash) + self.assertIsInstance(result, str, + "genhash() failed to return native string: %r" % (result,)) + + else: + # identify should reject these + self.assertFalse(self.do_identify(hash), + "identify() incorrectly identified hash belonging to " + "%s: %r" % (name, hash)) + + # verify should throw error + self.assertRaises(ValueError, self.do_verify, 'stub', hash, + __msg__= "verify() failed to throw error for hash " + "belonging to %s: %r" % (name, hash,)) + + # genhash() should throw error + self.assertRaises(ValueError, self.do_genhash, 'stub', hash, + __msg__= "genhash() failed to throw error for hash " + "belonging to %s: %r" % (name, hash)) + + def test_76_hash_border(self): + """test non-string hashes are rejected""" + # + # test hash=None is handled correctly + # + self.assertRaises(TypeError, self.do_identify, None) + self.assertRaises(TypeError, self.do_verify, 'stub', None) + + # NOTE: changed in 1.7 -- previously 'None' would be accepted when config strings not supported. + self.assertRaises(TypeError, self.do_genhash, 'stub', None) + + # + # test hash=int is rejected (picked as example of entirely wrong type) + # + self.assertRaises(TypeError, self.do_identify, 1) + self.assertRaises(TypeError, self.do_verify, 'stub', 1) + self.assertRaises(TypeError, self.do_genhash, 'stub', 1) + + # + # test hash='' is rejected for all but the plaintext hashes + # + for hash in [u(''), b'']: + if self.accepts_all_hashes: + # then it accepts empty string as well. + self.assertTrue(self.do_identify(hash)) + self.do_verify('stub', hash) + result = self.do_genhash('stub', hash) + self.check_returned_native_str(result, "genhash") + else: + # otherwise it should reject them + self.assertFalse(self.do_identify(hash), + "identify() incorrectly identified empty hash") + self.assertRaises(ValueError, self.do_verify, 'stub', hash, + __msg__="verify() failed to reject empty hash") + self.assertRaises(ValueError, self.do_genhash, 'stub', hash, + __msg__="genhash() failed to reject empty hash") + + # + # test identify doesn't throw decoding errors on 8-bit input + # + self.do_identify('\xe2\x82\xac\xc2\xa5$') # utf-8 + self.do_identify('abc\x91\x00') # non-utf8 + + #=================================================================== + # test parsehash() + #=================================================================== + + #: optional list of known parse hash results for hasher + known_parsehash_results = [] + + def require_parsehash(self): + if not hasattr(self.handler, "parsehash"): + raise SkipTest("parsehash() not implemented") + + def test_70_parsehash(self): + """ + parsehash() + """ + # TODO: would like to enhance what this test covers + + self.require_parsehash() + handler = self.handler + + # calls should succeed, and return dict + hash = self.do_encrypt("stub") + result = handler.parsehash(hash) + self.assertIsInstance(result, dict) + # TODO: figure out what invariants we can reliably parse, + # or maybe make subclasses specify that? + + # w/ checksum=False, should omit that key + result2 = handler.parsehash(hash, checksum=False) + correct2 = result.copy() + correct2.pop("checksum", None) + self.assertEqual(result2, correct2) + + # w/ sanitize=True + # correct output should mask salt / checksum; + # but all else should be the same + result3 = handler.parsehash(hash, sanitize=True) + correct3 = result.copy() + if PY2: + # silence warning about bytes & unicode not comparing + # (sanitize may convert bytes into base64 text) + warnings.filterwarnings("ignore", ".*unequal comparison failed to convert.*", + category=UnicodeWarning) + for key in ("salt", "checksum"): + if key in result3: + self.assertNotEqual(result3[key], correct3[key]) + self.assert_is_masked(result3[key]) + correct3[key] = result3[key] + self.assertEqual(result3, correct3) + + def assert_is_masked(self, value): + """ + check value properly masked by :func:`passlib.utils.mask_value` + """ + if value is None: + return + self.assertIsInstance(value, unicode) + # assumes mask_value() defaults will never show more than chars (4); + # and show nothing if size less than 1/ (8). + ref = value if len(value) < 8 else value[4:] + if set(ref) == set(["*"]): + return True + raise self.fail("value not masked: %r" % value) + + def test_71_parsehash_results(self): + """ + parsehash() -- known outputs + """ + self.require_parsehash() + samples = self.known_parsehash_results + if not samples: + raise self.skipTest("no samples present") + # XXX: expand to test w/ checksum=False and/or sanitize=True? + # or read "_unsafe_settings"? + for hash, correct in self.known_parsehash_results: + result = self.handler.parsehash(hash) + self.assertEqual(result, correct, "hash=%r:" % hash) + + #=================================================================== + # fuzz testing + #=================================================================== + def test_77_fuzz_input(self, threaded=False): + """fuzz testing -- random passwords and options + + This test attempts to perform some basic fuzz testing of the hash, + based on whatever information can be found about it. + It does as much as it can within a fixed amount of time + (defaults to 1 second, but can be overridden via $PASSLIB_TEST_FUZZ_TIME). + It tests the following: + + * randomly generated passwords including extended unicode chars + * randomly selected rounds values (if rounds supported) + * randomly selected salt sizes (if salts supported) + * randomly selected identifiers (if multiple found) + * runs output of selected backend against other available backends + (if any) to detect errors occurring between different backends. + * runs output against other "external" verifiers such as OS crypt() + + :param report_thread_state: + if true, writes state of loop to current_thread().passlib_fuzz_state. + used to help debug multi-threaded fuzz test issues (below) + """ + if self.handler.is_disabled: + raise self.skipTest("not applicable") + + # gather info + from passlib.utils import tick + max_time = self.max_fuzz_time + if max_time <= 0: + raise self.skipTest("disabled by test mode") + verifiers = self.get_fuzz_verifiers(threaded=threaded) + def vname(v): + return (v.__doc__ or v.__name__).splitlines()[0] + + # init rng -- using separate one for each thread + # so things are predictable for given RANDOM_TEST_SEED + # (relies on test_78_fuzz_threading() to give threads unique names) + if threaded: + thread_name = threading.current_thread().name + else: + thread_name = "fuzz test" + rng = self.getRandom(name=thread_name) + generator = self.FuzzHashGenerator(self, rng) + + # do as many tests as possible for max_time seconds + log.debug("%s: %s: started; max_time=%r verifiers=%d (%s)", + self.descriptionPrefix, thread_name, max_time, len(verifiers), + ", ".join(vname(v) for v in verifiers)) + start = tick() + stop = start + max_time + count = 0 + while tick() <= stop: + # generate random password & options + opts = generator.generate() + secret = opts['secret'] + other = opts['other'] + settings = opts['settings'] + ctx = opts['context'] + if ctx: + settings['context'] = ctx + + # create new hash + hash = self.do_encrypt(secret, **settings) + ##log.debug("fuzz test: hash=%r secret=%r other=%r", + ## hash, secret, other) + + # run through all verifiers we found. + for verify in verifiers: + name = vname(verify) + result = verify(secret, hash, **ctx) + if result == "skip": # let verifiers signal lack of support + continue + assert result is True or result is False + if not result: + raise self.failureException("failed to verify against %r verifier: " + "secret=%r config=%r hash=%r" % + (name, secret, settings, hash)) + # occasionally check that some other secrets WON'T verify + # against this hash. + if rng.random() < .1: + result = verify(other, hash, **ctx) + if result and result != "skip": + raise self.failureException("was able to verify wrong " + "password using %s: wrong_secret=%r real_secret=%r " + "config=%r hash=%r" % (name, other, secret, settings, hash)) + count += 1 + + log.debug("%s: %s: done; elapsed=%r count=%r", + self.descriptionPrefix, thread_name, tick() - start, count) + + def test_78_fuzz_threading(self): + """multithreaded fuzz testing -- random password & options using multiple threads + + run test_77 simultaneously in multiple threads + in an attempt to detect any concurrency issues + (e.g. the bug fixed by pybcrypt 0.3) + """ + self.require_TEST_MODE("full") + import threading + + # check if this test should run + if self.handler.is_disabled: + raise self.skipTest("not applicable") + thread_count = self.fuzz_thread_count + if thread_count < 1 or self.max_fuzz_time <= 0: + raise self.skipTest("disabled by test mode") + + # buffer to hold errors thrown by threads + failed_lock = threading.Lock() + failed = [0] + + # launch threads, all of which run + # test_77_fuzz_input(), and see if any errors get thrown. + # if hash has concurrency issues, this should reveal it. + def wrapper(): + try: + self.test_77_fuzz_input(threaded=True) + except SkipTest: + pass + except: + with failed_lock: + failed[0] += 1 + raise + def launch(n): + cls = type(self) + name = "Fuzz-Thread-%d ('%s:%s.%s')" % (n, cls.__module__, cls.__name__, + self._testMethodName) + thread = threading.Thread(target=wrapper, name=name) + thread.setDaemon(True) + thread.start() + return thread + threads = [launch(n) for n in irange(thread_count)] + + # wait until all threads exit + timeout = self.max_fuzz_time * thread_count * 4 + stalled = 0 + for thread in threads: + thread.join(timeout) + if not thread.is_alive(): + continue + # XXX: not sure why this is happening, main one seems 1/4 times for sun_md5_crypt + log.error("%s timed out after %f seconds", thread.name, timeout) + stalled += 1 + + # if any thread threw an error, raise one ourselves. + if failed[0]: + raise self.fail("%d/%d threads failed concurrent fuzz testing " + "(see error log for details)" % (failed[0], thread_count)) + if stalled: + raise self.fail("%d/%d threads stalled during concurrent fuzz testing " + "(see error log for details)" % (stalled, thread_count)) + + #--------------------------------------------------------------- + # fuzz constants & helpers + #--------------------------------------------------------------- + + @property + def max_fuzz_time(self): + """amount of time to spend on fuzz testing""" + value = float(os.environ.get("PASSLIB_TEST_FUZZ_TIME") or 0) + if value: + return value + elif TEST_MODE(max="quick"): + return 0 + elif TEST_MODE(max="default"): + return 1 + else: + return 5 + + @property + def fuzz_thread_count(self): + """number of threads for threaded fuzz testing""" + value = int(os.environ.get("PASSLIB_TEST_FUZZ_THREADS") or 0) + if value: + return value + elif TEST_MODE(max="quick"): + return 0 + else: + return 10 + + #--------------------------------------------------------------- + # fuzz verifiers + #--------------------------------------------------------------- + + #: list of custom fuzz-test verifiers (in addition to hasher itself, + #: and backend-specific wrappers of hasher). each element is + #: name of method that will return None / a verifier callable. + fuzz_verifiers = ("fuzz_verifier_default",) + + def get_fuzz_verifiers(self, threaded=False): + """return list of password verifiers (including external libs) + + used by fuzz testing. + verifiers should be callable with signature + ``func(password: unicode, hash: ascii str) -> ok: bool``. + """ + handler = self.handler + verifiers = [] + + # call all methods starting with prefix in order to create + for method_name in self.fuzz_verifiers: + func = getattr(self, method_name)() + if func is not None: + verifiers.append(func) + + # create verifiers for any other available backends + # NOTE: skipping this under threading test, + # since backend switching isn't threadsafe (yet) + if hasattr(handler, "backends") and TEST_MODE("full") and not threaded: + def maker(backend): + def func(secret, hash): + orig_backend = handler.get_backend() + try: + handler.set_backend(backend) + return handler.verify(secret, hash) + finally: + handler.set_backend(orig_backend) + func.__name__ = "check_" + backend + "_backend" + func.__doc__ = backend + "-backend" + return func + for backend in iter_alt_backends(handler): + verifiers.append(maker(backend)) + + return verifiers + + def fuzz_verifier_default(self): + # test against self + def check_default(secret, hash, **ctx): + return self.do_verify(secret, hash, **ctx) + if self.backend: + check_default.__doc__ = self.backend + "-backend" + else: + check_default.__doc__ = "self" + return check_default + + #--------------------------------------------------------------- + # fuzz settings generation + #--------------------------------------------------------------- + class FuzzHashGenerator(object): + """ + helper which takes care of generating random + passwords & configuration options to test hash with. + separate from test class so we can create one per thread. + """ + #========================================================== + # class attrs + #========================================================== + + # alphabet for randomly generated passwords + password_alphabet = u('qwertyASDF1234<>.@*#! \u00E1\u0259\u0411\u2113') + + # encoding when testing bytes + password_encoding = "utf-8" + + # map of setting kwd -> method name. + # will ignore setting if method returns None. + # subclasses should make copy of dict. + settings_map = dict(rounds="random_rounds", + salt_size="random_salt_size", + ident="random_ident") + + # map of context kwd -> method name. + context_map = {} + + #========================================================== + # init / generation + #========================================================== + + def __init__(self, test, rng): + self.test = test + self.handler = test.handler + self.rng = rng + + def generate(self): + """ + generate random password and options for fuzz testing. + :returns: + `(secret, other_secret, settings_kwds, context_kwds)` + """ + def gendict(map): + out = {} + for key, meth in map.items(): + value = getattr(self, meth)() + if value is not None: + out[key] = value + return out + secret, other = self.random_password_pair() + return dict(secret=secret, + other=other, + settings=gendict(self.settings_map), + context=gendict(self.context_map), + ) + + #========================================================== + # helpers + #========================================================== + def randintgauss(self, lower, upper, mu, sigma): + """generate random int w/ gauss distirbution""" + value = self.rng.normalvariate(mu, sigma) + return int(limit(value, lower, upper)) + + #========================================================== + # settings generation + #========================================================== + + def random_rounds(self): + handler = self.handler + if not has_rounds_info(handler): + return None + default = handler.default_rounds or handler.min_rounds + lower = handler.min_rounds + if handler.rounds_cost == "log2": + upper = default + else: + upper = min(default*2, handler.max_rounds) + return self.randintgauss(lower, upper, default, default*.5) + + def random_salt_size(self): + handler = self.handler + if not (has_salt_info(handler) and 'salt_size' in handler.setting_kwds): + return None + default = handler.default_salt_size + lower = handler.min_salt_size + upper = handler.max_salt_size or default*4 + return self.randintgauss(lower, upper, default, default*.5) + + def random_ident(self): + rng = self.rng + handler = self.handler + if 'ident' not in handler.setting_kwds or not hasattr(handler, "ident_values"): + return None + if rng.random() < .5: + return None + # resolve wrappers before reading values + handler = getattr(handler, "wrapped", handler) + return rng.choice(handler.ident_values) + + #========================================================== + # fuzz password generation + #========================================================== + def random_password_pair(self): + """generate random password, and non-matching alternate password""" + secret = self.random_password() + while True: + other = self.random_password() + if self.accept_password_pair(secret, other): + break + rng = self.rng + if rng.randint(0,1): + secret = secret.encode(self.password_encoding) + if rng.randint(0,1): + other = other.encode(self.password_encoding) + return secret, other + + def random_password(self): + """generate random passwords for fuzz testing""" + # occasionally try an empty password + rng = self.rng + if rng.random() < .0001: + return u('') + + # check if truncate size needs to be considered + handler = self.handler + truncate_size = handler.truncate_error and handler.truncate_size + max_size = truncate_size or 999999 + + # pick endpoint + if max_size < 50 or rng.random() < .5: + # chance of small password (~15 chars) + size = self.randintgauss(1, min(max_size, 50), 15, 15) + else: + # otherwise large password (~70 chars) + size = self.randintgauss(50, min(max_size, 99), 70, 20) + + # generate random password + result = getrandstr(rng, self.password_alphabet, size) + + # trim ones that encode past truncate point. + if truncate_size and isinstance(result, unicode): + while len(result.encode("utf-8")) > truncate_size: + result = result[:-1] + + return result + + def accept_password_pair(self, secret, other): + """verify fuzz pair contains different passwords""" + return secret != other + + #========================================================== + # eoc FuzzGenerator + #========================================================== + + #=================================================================== + # "disabled hasher" api + #=================================================================== + + def test_disable_and_enable(self): + """.disable() / .enable() methods""" + # + # setup + # + handler = self.handler + if not handler.is_disabled: + self.assertFalse(hasattr(handler, "disable")) + self.assertFalse(hasattr(handler, "enable")) + self.assertFalse(self.disabled_contains_salt) + raise self.skipTest("not applicable") + + # + # disable() + # + + # w/o existing hash + disabled_default = handler.disable() + self.assertIsInstance(disabled_default, str, + msg="disable() must return native string") + self.assertTrue(handler.identify(disabled_default), + msg="identify() didn't recognize disable() result: %r" % (disabled_default)) + + # w/ existing hash + stub = self.getRandom().choice(self.known_other_hashes)[1] + disabled_stub = handler.disable(stub) + self.assertIsInstance(disabled_stub, str, + msg="disable() must return native string") + self.assertTrue(handler.identify(disabled_stub), + msg="identify() didn't recognize disable() result: %r" % (disabled_stub)) + + # + # enable() + # + + # w/o original hash + self.assertRaisesRegex(ValueError, "cannot restore original hash", + handler.enable, disabled_default) + + # w/ original hash + try: + result = handler.enable(disabled_stub) + error = None + except ValueError as e: + result = None + error = e + + if error is None: + # if supports recovery, should have returned stub (e.g. unix_disabled); + self.assertIsInstance(result, str, + msg="enable() must return native string") + self.assertEqual(result, stub) + else: + # if doesn't, should have thrown appropriate error + self.assertIsInstance(error, ValueError) + self.assertRegex("cannot restore original hash", str(error)) + + # + # test repeating disable() & salting state + # + + # repeating disabled + disabled_default2 = handler.disable() + if self.disabled_contains_salt: + # should return new salt for each call (e.g. django_disabled) + self.assertNotEqual(disabled_default2, disabled_default) + elif error is None: + # should return same result for each hash, but unique across hashes + self.assertEqual(disabled_default2, disabled_default) + + # repeating same hash ... + disabled_stub2 = handler.disable(stub) + if self.disabled_contains_salt: + # ... should return different string (if salted) + self.assertNotEqual(disabled_stub2, disabled_stub) + else: + # ... should return same string + self.assertEqual(disabled_stub2, disabled_stub) + + # using different hash ... + disabled_other = handler.disable(stub + 'xxx') + if self.disabled_contains_salt or error is None: + # ... should return different string (if salted or hash encoded) + self.assertNotEqual(disabled_other, disabled_stub) + else: + # ... should return same string + self.assertEqual(disabled_other, disabled_stub) + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# HandlerCase mixins providing additional tests for certain hashes +#============================================================================= +class OsCryptMixin(HandlerCase): + """helper used by create_backend_case() which adds additional features + to test the os_crypt backend. + + * if crypt support is missing, inserts fake crypt support to simulate + a working safe_crypt, to test passlib's codepath as fully as possible. + + * extra tests to verify non-conformant crypt implementations are handled + correctly. + + * check that native crypt support is detected correctly for known platforms. + """ + #=================================================================== + # class attrs + #=================================================================== + + # platforms that are known to support / not support this hash natively. + # list of (platform_regex, True|False|None) entries. + platform_crypt_support = [] + + #=================================================================== + # instance attrs + #=================================================================== + __unittest_skip = True + + # force this backend + backend = "os_crypt" + + # flag read by HandlerCase to detect if fake os crypt is enabled. + using_patched_crypt = False + + #=================================================================== + # setup + #=================================================================== + def setUp(self): + assert self.backend == "os_crypt" + if not self.handler.has_backend("os_crypt"): + # XXX: currently, any tests that use this are skipped entirely! (see issue 120) + self._patch_safe_crypt() + super(OsCryptMixin, self).setUp() + + @classmethod + def _get_safe_crypt_handler_backend(cls): + """ + return (handler, backend) pair to use for faking crypt.crypt() support for hash. + backend will be None if none availabe. + """ + # find handler that generates safe_crypt() compatible hash + handler = unwrap_handler(cls.handler) + + # hack to prevent recursion issue when .has_backend() is called + handler.get_backend() + + # find backend which isn't os_crypt + alt_backend = get_alt_backend(handler, "os_crypt") + return handler, alt_backend + + @property + def has_os_crypt_fallback(self): + """ + test if there's a fallback handler to test against if os_crypt can't support + a specified secret (may be explicitly set to False for some subclasses) + """ + return self._get_safe_crypt_handler_backend()[0] is not None + + def _patch_safe_crypt(self): + """if crypt() doesn't support current hash alg, this patches + safe_crypt() so that it transparently uses another one of the handler's + backends, so that we can go ahead and test as much of code path + as possible. + """ + # find handler & backend + handler, alt_backend = self._get_safe_crypt_handler_backend() + if not alt_backend: + raise AssertionError("handler has no available alternate backends!") + + # create subclass of handler, which we swap to an alternate backend + alt_handler = handler.using() + alt_handler.set_backend(alt_backend) + + def crypt_stub(secret, hash): + hash = alt_handler.genhash(secret, hash) + assert isinstance(hash, str) + return hash + + import passlib.utils as mod + self.patchAttr(mod, "_crypt", crypt_stub) + self.using_patched_crypt = True + + @classmethod + def _get_skip_backend_reason(cls, backend): + """ + make sure os_crypt backend is tested + when it's known os_crypt will be faked by _patch_safe_crypt() + """ + assert backend == "os_crypt" + reason = super(OsCryptMixin, cls)._get_skip_backend_reason(backend) + + from passlib.utils import has_crypt + if reason == cls._BACKEND_NOT_AVAILABLE and has_crypt: + if TEST_MODE("full") and cls._get_safe_crypt_handler_backend()[1]: + # in this case, _patch_safe_crypt() will monkeypatch os_crypt + # to use another backend, just so we can test os_crypt fully. + return None + else: + return "hash not supported by os crypt()" + + return reason + + #=================================================================== + # custom tests + #=================================================================== + + # TODO: turn into decorator, and use mock library. + def _use_mock_crypt(self): + """ + patch passlib.utils.safe_crypt() so it returns mock value for duration of test. + returns function whose .return_value controls what's returned. + this defaults to None. + """ + import passlib.utils as mod + + def mock_crypt(secret, config): + # let 'test' string through so _load_os_crypt_backend() will still work + if secret == "test": + return mock_crypt.__wrapped__(secret, config) + else: + return mock_crypt.return_value + + mock_crypt.__wrapped__ = mod._crypt + mock_crypt.return_value = None + + self.patchAttr(mod, "_crypt", mock_crypt) + + return mock_crypt + + def test_80_faulty_crypt(self): + """test with faulty crypt()""" + hash = self.get_sample_hash()[1] + exc_types = (exc.InternalBackendError,) + mock_crypt = self._use_mock_crypt() + + def test(value): + # set safe_crypt() to return specified value, and + # make sure assertion error is raised by handler. + mock_crypt.return_value = value + self.assertRaises(exc_types, self.do_genhash, "stub", hash) + self.assertRaises(exc_types, self.do_encrypt, "stub") + self.assertRaises(exc_types, self.do_verify, "stub", hash) + + test('$x' + hash[2:]) # detect wrong prefix + test(hash[:-1]) # detect too short + test(hash + 'x') # detect too long + + def test_81_crypt_fallback(self): + """test per-call crypt() fallback""" + + # mock up safe_crypt to return None + mock_crypt = self._use_mock_crypt() + mock_crypt.return_value = None + + if self.has_os_crypt_fallback: + # handler should have a fallback to use when os_crypt backend refuses to handle secret. + h1 = self.do_encrypt("stub") + h2 = self.do_genhash("stub", h1) + self.assertEqual(h2, h1) + self.assertTrue(self.do_verify("stub", h1)) + else: + # handler should give up + from passlib.exc import InternalBackendError as err_type + hash = self.get_sample_hash()[1] + self.assertRaises(err_type, self.do_encrypt, 'stub') + self.assertRaises(err_type, self.do_genhash, 'stub', hash) + self.assertRaises(err_type, self.do_verify, 'stub', hash) + + @doesnt_require_backend + def test_82_crypt_support(self): + """ + test platform-specific crypt() support detection + + NOTE: this is mainly just a sanity check to ensure the runtime + detection is functioning correctly on some known platforms, + so that we can feel more confident it'll work right on unknown ones. + """ + + # skip wrapper handlers, won't ever have crypt support + if hasattr(self.handler, "orig_prefix"): + raise self.skipTest("not applicable to wrappers") + + # look for first entry that matches current system + # XXX: append "/" + platform.release() to string? + # XXX: probably should rework to support rows being dicts w/ "minver" / "maxver" keys, + # instead of hack where we add major # as part of platform regex. + using_backend = not self.using_patched_crypt + name = self.handler.name + platform = sys.platform + for pattern, expected in self.platform_crypt_support: + if re.match(pattern, platform): + break + else: + raise self.skipTest("no data for %r platform (current host support = %r)" % + (platform, using_backend)) + + # rules can use "state=None" to signal varied support; + # e.g. platform='freebsd8' ... sha256_crypt not added until 8.3 + if expected is None: + raise self.skipTest("varied support on %r platform (current host support = %r)" % + (platform, using_backend)) + + # compare expectation vs reality + if expected == using_backend: + pass + elif expected: + self.fail("expected %r platform would have native support for %r" % + (platform, name)) + else: + self.fail("did not expect %r platform would have native support for %r" % + (platform, name)) + + #=================================================================== + # fuzzy verified support -- add additional verifier that uses os crypt() + #=================================================================== + + def fuzz_verifier_crypt(self): + """test results against OS crypt()""" + + # don't use this if we're faking safe_crypt (pointless test), + # or if handler is a wrapper (only original handler will be supported by os) + handler = self.handler + if self.using_patched_crypt or hasattr(handler, "wrapped"): + return None + + # create a wrapper for fuzzy verified to use + from crypt import crypt + from passlib.utils import _safe_crypt_lock + encoding = self.FuzzHashGenerator.password_encoding + + def check_crypt(secret, hash): + """stdlib-crypt""" + if not self.crypt_supports_variant(hash): + return "skip" + # XXX: any reason not to use safe_crypt() here? or just want to test against bare metal? + secret = to_native_str(secret, encoding) + with _safe_crypt_lock: + return crypt(secret, hash) == hash + + return check_crypt + + def crypt_supports_variant(self, hash): + """ + fuzzy_verified_crypt() helper -- + used to determine if os crypt() supports a particular hash variant. + """ + return True + + #=================================================================== + # eoc + #=================================================================== + +class UserHandlerMixin(HandlerCase): + """helper for handlers w/ 'user' context kwd; mixin for HandlerCase + + this overrides the HandlerCase test harness methods + so that a username is automatically inserted to hash/verify + calls. as well, passing in a pair of strings as the password + will be interpreted as (secret,user) + """ + #=================================================================== + # option flags + #=================================================================== + default_user = "user" + requires_user = True + user_case_insensitive = False + + #=================================================================== + # instance attrs + #=================================================================== + __unittest_skip = True + + #=================================================================== + # custom tests + #=================================================================== + def test_80_user(self): + """test user context keyword""" + handler = self.handler + password = 'stub' + hash = handler.hash(password, user=self.default_user) + + if self.requires_user: + self.assertRaises(TypeError, handler.hash, password) + self.assertRaises(TypeError, handler.genhash, password, hash) + self.assertRaises(TypeError, handler.verify, password, hash) + else: + # e.g. cisco_pix works with or without one. + handler.hash(password) + handler.genhash(password, hash) + handler.verify(password, hash) + + def test_81_user_case(self): + """test user case sensitivity""" + lower = self.default_user.lower() + upper = lower.upper() + hash = self.do_encrypt('stub', context=dict(user=lower)) + if self.user_case_insensitive: + self.assertTrue(self.do_verify('stub', hash, user=upper), + "user should not be case sensitive") + else: + self.assertFalse(self.do_verify('stub', hash, user=upper), + "user should be case sensitive") + + def test_82_user_salt(self): + """test user used as salt""" + config = self.do_stub_encrypt() + h1 = self.do_genhash('stub', config, user='admin') + h2 = self.do_genhash('stub', config, user='admin') + self.assertEqual(h2, h1) + h3 = self.do_genhash('stub', config, user='root') + self.assertNotEqual(h3, h1) + + # TODO: user size? kinda dicey, depends on algorithm. + + #=================================================================== + # override test helpers + #=================================================================== + def populate_context(self, secret, kwds): + """insert username into kwds""" + if isinstance(secret, tuple): + secret, user = secret + elif not self.requires_user: + return secret + else: + user = self.default_user + if 'user' not in kwds: + kwds['user'] = user + return secret + + #=================================================================== + # modify fuzz testing + #=================================================================== + class FuzzHashGenerator(HandlerCase.FuzzHashGenerator): + + context_map = HandlerCase.FuzzHashGenerator.context_map.copy() + context_map.update(user="random_user") + + user_alphabet = u("asdQWE123") + + def random_user(self): + rng = self.rng + if not self.test.requires_user and rng.random() < .1: + return None + return getrandstr(rng, self.user_alphabet, rng.randint(2,10)) + + #=================================================================== + # eoc + #=================================================================== + +class EncodingHandlerMixin(HandlerCase): + """helper for handlers w/ 'encoding' context kwd; mixin for HandlerCase + + this overrides the HandlerCase test harness methods + so that an encoding can be inserted to hash/verify + calls by passing in a pair of strings as the password + will be interpreted as (secret,encoding) + """ + #=================================================================== + # instance attrs + #=================================================================== + __unittest_skip = True + + # restrict stock passwords & fuzz alphabet to latin-1, + # so different encodings can be tested safely. + stock_passwords = [ + u("test"), + b"test", + u("\u00AC\u00BA"), + ] + + class FuzzHashGenerator(HandlerCase.FuzzHashGenerator): + + password_alphabet = u('qwerty1234<>.@*#! \u00AC') + + def populate_context(self, secret, kwds): + """insert encoding into kwds""" + if isinstance(secret, tuple): + secret, encoding = secret + kwds.setdefault('encoding', encoding) + return secret + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# warnings helpers +#============================================================================= +class reset_warnings(warnings.catch_warnings): + """catch_warnings() wrapper which clears warning registry & filters""" + + def __init__(self, reset_filter="always", reset_registry=".*", **kwds): + super(reset_warnings, self).__init__(**kwds) + self._reset_filter = reset_filter + self._reset_registry = re.compile(reset_registry) if reset_registry else None + + def __enter__(self): + # let parent class archive filter state + ret = super(reset_warnings, self).__enter__() + + # reset the filter to list everything + if self._reset_filter: + warnings.resetwarnings() + warnings.simplefilter(self._reset_filter) + + # archive and clear the __warningregistry__ key for all modules + # that match the 'reset' pattern. + pattern = self._reset_registry + if pattern: + backup = self._orig_registry = {} + for name, mod in list(sys.modules.items()): + if mod is None or not pattern.match(name): + continue + reg = getattr(mod, "__warningregistry__", None) + if reg: + backup[name] = reg.copy() + reg.clear() + return ret + + def __exit__(self, *exc_info): + # restore warning registry for all modules + pattern = self._reset_registry + if pattern: + # restore registry backup, clearing all registry entries that we didn't archive + backup = self._orig_registry + for name, mod in list(sys.modules.items()): + if mod is None or not pattern.match(name): + continue + reg = getattr(mod, "__warningregistry__", None) + if reg: + reg.clear() + orig = backup.get(name) + if orig: + if reg is None: + setattr(mod, "__warningregistry__", orig) + else: + reg.update(orig) + super(reset_warnings, self).__exit__(*exc_info) + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/totp.py b/ansible/lib/python3.11/site-packages/passlib/totp.py new file mode 100644 index 000000000..9ad500087 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/totp.py @@ -0,0 +1,1908 @@ +"""passlib.totp -- TOTP / RFC6238 / Google Authenticator utilities.""" +#============================================================================= +# imports +#============================================================================= +from __future__ import absolute_import, division, print_function +from passlib.utils.compat import PY3 +# core +import base64 +import calendar +import json +import logging; log = logging.getLogger(__name__) +import math +import struct +import sys +import time as _time +import re +if PY3: + from urllib.parse import urlparse, parse_qsl, quote, unquote +else: + from urllib import quote, unquote + from urlparse import urlparse, parse_qsl +from warnings import warn +# site +try: + # TOTP encrypted keys only supported if cryptography (https://cryptography.io) is installed + from cryptography.hazmat.backends import default_backend as _cg_default_backend + import cryptography.hazmat.primitives.ciphers.algorithms + import cryptography.hazmat.primitives.ciphers.modes + from cryptography.hazmat.primitives import ciphers as _cg_ciphers + del cryptography +except ImportError: + log.debug("can't import 'cryptography' package, totp encryption disabled") + _cg_ciphers = _cg_default_backend = None +# pkg +from passlib import exc +from passlib.exc import TokenError, MalformedTokenError, InvalidTokenError, UsedTokenError +from passlib.utils import (to_unicode, to_bytes, consteq, + getrandbytes, rng, SequenceMixin, xor_bytes, getrandstr) +from passlib.utils.binary import BASE64_CHARS, b32encode, b32decode +from passlib.utils.compat import (u, unicode, native_string_types, bascii_to_str, int_types, num_types, + irange, byte_elem_value, UnicodeIO, suppress_cause) +from passlib.utils.decor import hybrid_method, memoized_property +from passlib.crypto.digest import lookup_hash, compile_hmac, pbkdf2_hmac +from passlib.hash import pbkdf2_sha256 +# local +__all__ = [ + # frontend classes + "AppWallet", + "TOTP", + + # errors (defined in passlib.exc, but exposed here for convenience) + "TokenError", + "MalformedTokenError", + "InvalidTokenError", + "UsedTokenError", + + # internal helper classes + "TotpToken", + "TotpMatch", +] + +#============================================================================= +# HACK: python < 2.7.4's urlparse() won't parse query strings unless the url scheme +# is one of the schemes in the urlparse.uses_query list. 2.7 abandoned +# this, and parses query if present, regardless of the scheme. +# as a workaround for older versions, we add "otpauth" to the known list. +# this was fixed by https://bugs.python.org/issue9374, in 2.7.4 release. +#============================================================================= +if sys.version_info < (2,7,4): + from urlparse import uses_query + if "otpauth" not in uses_query: + uses_query.append("otpauth") + log.debug("registered 'otpauth' scheme with urlparse.uses_query") + del uses_query + +#============================================================================= +# internal helpers +#============================================================================= + +#----------------------------------------------------------------------------- +# token parsing / rendering helpers +#----------------------------------------------------------------------------- + +#: regex used to clean whitespace from tokens & keys +_clean_re = re.compile(u(r"\s|[-=]"), re.U) + +_chunk_sizes = [4,6,5] + +def _get_group_size(klen): + """ + helper for group_string() -- + calculates optimal size of group for given string size. + """ + # look for exact divisor + for size in _chunk_sizes: + if not klen % size: + return size + # fallback to divisor with largest remainder + # (so chunks are as close to even as possible) + best = _chunk_sizes[0] + rem = 0 + for size in _chunk_sizes: + if klen % size > rem: + best = size + rem = klen % size + return best + +def group_string(value, sep="-"): + """ + reformat string into (roughly) evenly-sized groups, separated by **sep**. + useful for making tokens & keys easier to read by humans. + """ + klen = len(value) + size = _get_group_size(klen) + return sep.join(value[o:o+size] for o in irange(0, klen, size)) + +#----------------------------------------------------------------------------- +# encoding helpers +#----------------------------------------------------------------------------- + +def _decode_bytes(key, format): + """ + internal TOTP() helper -- + decodes key according to specified format. + """ + if format == "raw": + if not isinstance(key, bytes): + raise exc.ExpectedTypeError(key, "bytes", "key") + return key + # for encoded data, key must be either unicode or ascii-encoded bytes, + # and must contain a hex or base32 string. + key = to_unicode(key, param="key") + key = _clean_re.sub("", key).encode("utf-8") # strip whitespace & hypens + if format == "hex" or format == "base16": + return base64.b16decode(key.upper()) + elif format == "base32": + return b32decode(key) + # XXX: add base64 support? + else: + raise ValueError("unknown byte-encoding format: %r" % (format,)) + +#============================================================================= +# OTP management +#============================================================================= + +#: flag for detecting if encrypted totp support is present +AES_SUPPORT = bool(_cg_ciphers) + +#: regex for validating secret tags +_tag_re = re.compile("(?i)^[a-z0-9][a-z0-9_.-]*$") + +class AppWallet(object): + """ + This class stores application-wide secrets that can be used + to encrypt & decrypt TOTP keys for storage. + It's mostly an internal detail, applications usually just need + to pass ``secrets`` or ``secrets_path`` to :meth:`TOTP.using`. + + .. seealso:: + + :ref:`totp-storing-instances` for more details on this workflow. + + Arguments + ========= + :param secrets: + Dict of application secrets to use when encrypting/decrypting + stored TOTP keys. This should include a secret to use when encrypting + new keys, but may contain additional older secrets to decrypt + existing stored keys. + + The dict should map tags -> secrets, so that each secret is identified + by a unique tag. This tag will be stored along with the encrypted + key in order to determine which secret should be used for decryption. + Tag should be string that starts with regex range ``[a-z0-9]``, + and the remaining characters must be in ``[a-z0-9_.-]``. + + It is recommended to use something like a incremental counter + ("1", "2", ...), an ISO date ("2016-01-01", "2016-05-16", ...), + or a timestamp ("19803495", "19813495", ...) when assigning tags. + + This mapping be provided in three formats: + + * A python dict mapping tag -> secret + * A JSON-formatted string containing the dict + * A multiline string with the format ``"tag: value\\ntag: value\\n..."`` + + (This last format is mainly useful when loading from a text file via **secrets_path**) + + .. seealso:: :func:`generate_secret` to create a secret with sufficient entropy + + :param secrets_path: + Alternately, callers can specify a separate file where the + application-wide secrets are stored, using either of the string + formats described in **secrets**. + + :param default_tag: + Specifies which tag in **secrets** should be used as the default + for encrypting new keys. If omitted, the tags will be sorted, + and the largest tag used as the default. + + if all tags are numeric, they will be sorted numerically; + otherwise they will be sorted alphabetically. + this permits tags to be assigned numerically, + or e.g. using ``YYYY-MM-DD`` dates. + + :param encrypt_cost: + Optional time-cost factor for key encryption. + This value corresponds to log2() of the number of PBKDF2 + rounds used. + + .. warning:: + + The application secret(s) should be stored in a secure location by + your application, and each secret should contain a large amount + of entropy (to prevent brute-force attacks if the encrypted keys + are leaked). + + :func:`generate_secret` is provided as a convenience helper + to generate a new application secret of suitable size. + + Best practice is to load these values from a file via **secrets_path**, + and then have your application give up permission to read this file + once it's running. + + Public Methods + ============== + .. autoattribute:: has_secrets + .. autoattribute:: default_tag + + Semi-Private Methods + ==================== + The following methods are used internally by the :class:`TOTP` + class in order to encrypt & decrypt keys using the provided application + secrets. They will generally not be publically useful, and may have their + API changed periodically. + + .. automethod:: get_secret + .. automethod:: encrypt_key + .. automethod:: decrypt_key + """ + #======================================================================== + # instance attrs + #======================================================================== + + #: default salt size for encrypt_key() output + salt_size = 12 + + #: default cost (log2 of pbkdf2 rounds) for encrypt_key() output + #: NOTE: this is relatively low, since the majority of the security + #: relies on a high entropy secret to pass to AES. + encrypt_cost = 14 + + #: map of secret tag -> secret bytes + _secrets = None + + #: tag for default secret + default_tag = None + + #======================================================================== + # init + #======================================================================== + def __init__(self, secrets=None, default_tag=None, encrypt_cost=None, + secrets_path=None): + + # TODO: allow a lot more things to be customized from here, + # e.g. setting default TOTP constructor options. + + # + # init cost + # + if encrypt_cost is not None: + if isinstance(encrypt_cost, native_string_types): + encrypt_cost = int(encrypt_cost) + assert encrypt_cost >= 0 + self.encrypt_cost = encrypt_cost + + # + # init secrets map + # + + # load secrets from file (if needed) + if secrets_path is not None: + if secrets is not None: + raise TypeError("'secrets' and 'secrets_path' are mutually exclusive") + secrets = open(secrets_path, "rt").read() + + # parse & store secrets + secrets = self._secrets = self._parse_secrets(secrets) + + # + # init default tag/secret + # + if secrets: + if default_tag is not None: + # verify that tag is present in map + self.get_secret(default_tag) + elif all(tag.isdigit() for tag in secrets): + default_tag = max(secrets, key=int) + else: + default_tag = max(secrets) + self.default_tag = default_tag + + def _parse_secrets(self, source): + """ + parse 'secrets' parameter + + :returns: + Dict[tag:str, secret:bytes] + """ + # parse string formats + # to make this easy to pass in configuration from a separate file, + # 'secrets' can be string using two formats -- json & "tag:value\n" + check_type = True + if isinstance(source, native_string_types): + if source.lstrip().startswith(("[", "{")): + # json list / dict + source = json.loads(source) + elif "\n" in source and ":" in source: + # multiline string containing series of "tag: value\n" rows; + # empty and "#\n" rows are ignored + def iter_pairs(source): + for line in source.splitlines(): + line = line.strip() + if line and not line.startswith("#"): + tag, secret = line.split(":", 1) + yield tag.strip(), secret.strip() + source = iter_pairs(source) + check_type = False + else: + raise ValueError("unrecognized secrets string format") + + # ensure we have iterable of (tag, value) pairs + if source is None: + return {} + elif isinstance(source, dict): + source = source.items() + # XXX: could support iterable of (tag,value) pairs, but not yet needed... + # elif check_type and (isinstance(source, str) or not isinstance(source, Iterable)): + elif check_type: + raise TypeError("'secrets' must be mapping, or list of items") + + # parse into final dict, normalizing contents + return dict(self._parse_secret_pair(tag, value) + for tag, value in source) + + def _parse_secret_pair(self, tag, value): + if isinstance(tag, native_string_types): + pass + elif isinstance(tag, int): + tag = str(tag) + else: + raise TypeError("tag must be unicode/string: %r" % (tag,)) + if not _tag_re.match(tag): + raise ValueError("tag contains invalid characters: %r" % (tag,)) + if not isinstance(value, bytes): + value = to_bytes(value, param="secret %r" % (tag,)) + if not value: + raise ValueError("tag contains empty secret: %r" % (tag,)) + return tag, value + + #======================================================================== + # accessing secrets + #======================================================================== + + @property + def has_secrets(self): + """whether at least one application secret is present""" + return self.default_tag is not None + + def get_secret(self, tag): + """ + resolve a secret tag to the secret (as bytes). + throws a KeyError if not found. + """ + secrets = self._secrets + if not secrets: + raise KeyError("no application secrets configured") + try: + return secrets[tag] + except KeyError: + raise suppress_cause(KeyError("unknown secret tag: %r" % (tag,))) + + #======================================================================== + # encrypted key helpers -- used internally by TOTP + #======================================================================== + + @staticmethod + def _cipher_aes_key(value, secret, salt, cost, decrypt=False): + """ + Internal helper for :meth:`encrypt_key` -- + handles lowlevel encryption/decryption. + + Algorithm details: + + This function uses PBKDF2-HMAC-SHA256 to generate a 32-byte AES key + and a 16-byte IV from the application secret & random salt. + It then uses AES-256-CTR to encrypt/decrypt the TOTP key. + + CTR mode was chosen over CBC because the main attack scenario here + is that the attacker has stolen the database, and is trying to decrypt a TOTP key + (the plaintext value here). To make it hard for them, we want every password + to decrypt to a potentially valid key -- thus need to avoid any authentication + or padding oracle attacks. While some random padding construction could be devised + to make this work for CBC mode, a stream cipher mode is just plain simpler. + OFB/CFB modes would also work here, but seeing as they have malleability + and cyclic issues (though remote and barely relevant here), + CTR was picked as the best overall choice. + """ + # make sure backend AES support is available + if _cg_ciphers is None: + raise RuntimeError("TOTP encryption requires 'cryptography' package " + "(https://cryptography.io)") + + # use pbkdf2 to derive both key (32 bytes) & iv (16 bytes) + # NOTE: this requires 2 sha256 blocks to be calculated. + keyiv = pbkdf2_hmac("sha256", secret, salt=salt, rounds=(1 << cost), keylen=48) + + # use AES-256-CTR to encrypt/decrypt input value + cipher = _cg_ciphers.Cipher(_cg_ciphers.algorithms.AES(keyiv[:32]), + _cg_ciphers.modes.CTR(keyiv[32:]), + _cg_default_backend()) + ctx = cipher.decryptor() if decrypt else cipher.encryptor() + return ctx.update(value) + ctx.finalize() + + def encrypt_key(self, key): + """ + Helper used to encrypt TOTP keys for storage. + + :param key: + TOTP key to encrypt, as raw bytes. + + :returns: + dict containing encrypted TOTP key & configuration parameters. + this format should be treated as opaque, and potentially subject + to change, though it is designed to be easily serialized/deserialized + (e.g. via JSON). + + .. note:: + + This function requires installation of the external + `cryptography `_ package. + + To give some algorithm details: This function uses AES-256-CTR to encrypt + the provided data. It takes the application secret and randomly generated salt, + and uses PBKDF2-HMAC-SHA256 to combine them and generate the AES key & IV. + """ + if not key: + raise ValueError("no key provided") + salt = getrandbytes(rng, self.salt_size) + cost = self.encrypt_cost + tag = self.default_tag + if not tag: + raise TypeError("no application secrets configured, can't encrypt OTP key") + ckey = self._cipher_aes_key(key, self.get_secret(tag), salt, cost) + # XXX: switch to base64? + return dict(v=1, c=cost, t=tag, s=b32encode(salt), k=b32encode(ckey)) + + def decrypt_key(self, enckey): + """ + Helper used to decrypt TOTP keys from storage format. + Consults configured secrets to decrypt key. + + :param source: + source object, as returned by :meth:`encrypt_key`. + + :returns: + ``(key, needs_recrypt)`` -- + + **key** will be the decrypted key, as bytes. + + **needs_recrypt** will be a boolean flag indicating + whether encryption cost or default tag is too old, + and henace that key needs re-encrypting before storing. + + .. note:: + + This function requires installation of the external + `cryptography `_ package. + """ + if not isinstance(enckey, dict): + raise TypeError("'enckey' must be dictionary") + version = enckey.get("v", None) + needs_recrypt = False + if version == 1: + _cipher_key = self._cipher_aes_key + else: + raise ValueError("missing / unrecognized 'enckey' version: %r" % (version,)) + tag = enckey['t'] + cost = enckey['c'] + key = _cipher_key( + value=b32decode(enckey['k']), + secret=self.get_secret(tag), + salt=b32decode(enckey['s']), + cost=cost, + ) + if cost != self.encrypt_cost or tag != self.default_tag: + needs_recrypt = True + return key, needs_recrypt + + #============================================================================= + # eoc + #============================================================================= + +#============================================================================= +# TOTP class +#============================================================================= + +#: helper to convert HOTP counter to bytes +_pack_uint64 = struct.Struct(">Q").pack + +#: helper to extract value from HOTP digest +_unpack_uint32 = struct.Struct(">I").unpack + +#: dummy bytes used as temp key for .using() method +_DUMMY_KEY = b"\x00" * 16 + +class TOTP(object): + """ + Helper for generating and verifying TOTP codes. + + Given a secret key and set of configuration options, this object + offers methods for token generation, token validation, and serialization. + It can also be used to track important persistent TOTP state, + such as the last counter used. + + This class accepts the following options + (only **key** and **format** may be specified as positional arguments). + + :arg str key: + The secret key to use. By default, should be encoded as + a base32 string (see **format** for other encodings). + + Exactly one of **key** or ``new=True`` must be specified. + + :arg str format: + The encoding used by the **key** parameter. May be one of: + ``"base32"`` (base32-encoded string), + ``"hex"`` (hexadecimal string), or ``"raw"`` (raw bytes). + Defaults to ``"base32"``. + + :param bool new: + If ``True``, a new key will be generated using :class:`random.SystemRandom`. + + Exactly one ``new=True`` or **key** must be specified. + + :param str label: + Label to associate with this token when generating a URI. + Displayed to user by most OTP client applications (e.g. Google Authenticator), + and typically has format such as ``"John Smith"`` or ``"jsmith@webservice.example.org"``. + Defaults to ``None``. + See :meth:`to_uri` for details. + + :param str issuer: + String identifying the token issuer (e.g. the domain name of your service). + Used internally by some OTP client applications (e.g. Google Authenticator) to distinguish entries + which otherwise have the same label. + Optional but strongly recommended if you're rendering to a URI. + Defaults to ``None``. + See :meth:`to_uri` for details. + + :param int size: + Number of bytes when generating new keys. Defaults to size of hash algorithm (e.g. 20 for SHA1). + + .. warning:: + + Overriding the default values for ``digits``, ``period``, or ``alg`` may + cause problems with some OTP client programs (such as Google Authenticator), + which may have these defaults hardcoded. + + :param int digits: + The number of digits in the generated / accepted tokens. Defaults to ``6``. + Must be in range [6 .. 10]. + + .. rst-class:: inline-title + .. caution:: + Due to a limitation of the HOTP algorithm, the 10th digit can only take on values 0 .. 2, + and thus offers very little extra security. + + :param str alg: + Name of hash algorithm to use. Defaults to ``"sha1"``. + ``"sha256"`` and ``"sha512"`` are also accepted, per :rfc:`6238`. + + :param int period: + The time-step period to use, in integer seconds. Defaults to ``30``. + + .. + See the passlib documentation for a full list of attributes & methods. + """ + #============================================================================= + # class attrs + #============================================================================= + + #: minimum number of bytes to allow in key, enforced by passlib. + # XXX: see if spec says anything relevant to this. + _min_key_size = 10 + + #: minimum & current serialization version (may be set independently by subclasses) + min_json_version = json_version = 1 + + #: AppWallet that this class will use for encrypting/decrypting keys. + #: (can be overwritten via the :meth:`TOTP.using()` constructor) + wallet = None + + #: function to get system time in seconds, as needed by :meth:`generate` and :meth:`verify`. + #: defaults to :func:`time.time`, but can be overridden on a per-instance basis. + now = _time.time + + #============================================================================= + # instance attrs + #============================================================================= + + #--------------------------------------------------------------------------- + # configuration attrs + #--------------------------------------------------------------------------- + + #: [private] secret key as raw :class:`!bytes` + #: see .key property for public access. + _key = None + + #: [private] cached copy of encrypted secret, + #: so .to_json() doesn't have to re-encrypt on each call. + _encrypted_key = None + + #: [private] cached copy of keyed HMAC function, + #: so ._generate() doesn't have to rebuild this each time + #: ._find_match() invokes it. + _keyed_hmac = None + + #: number of digits in the generated tokens. + digits = 6 + + #: name of hash algorithm in use (e.g. ``"sha1"``) + alg = "sha1" + + #: default label for :meth:`to_uri` + label = None + + #: default issuer for :meth:`to_uri` + issuer = None + + #: number of seconds per counter step. + #: *(TOTP uses an internal time-derived counter which + #: increments by 1 every* :attr:`!period` *seconds)*. + period = 30 + + #--------------------------------------------------------------------------- + # state attrs + #--------------------------------------------------------------------------- + + #: Flag set by deserialization methods to indicate the object needs to be re-serialized. + #: This can be for a number of reasons -- encoded using deprecated format, + #: or encrypted using a deprecated key or too few rounds. + changed = False + + #============================================================================= + # prototype construction + #============================================================================= + @classmethod + def using(cls, digits=None, alg=None, period=None, + issuer=None, wallet=None, now=None, **kwds): + """ + Dynamically create subtype of :class:`!TOTP` class + which has the specified defaults set. + + :parameters: **digits, alg, period, issuer**: + + All these options are the same as in the :class:`TOTP` constructor, + and the resulting class will use any values you specify here + as the default for all TOTP instances it creates. + + :param wallet: + Optional :class:`AppWallet` that will be used for encrypting/decrypting keys. + + :param secrets, secrets_path, encrypt_cost: + + If specified, these options will be passed to the :class:`AppWallet` constructor, + allowing you to directly specify the secret keys that should be used + to encrypt & decrypt stored keys. + + :returns: + subclass of :class:`!TOTP`. + + This method is useful for creating a TOTP class configured + to use your application's secrets for encrypting & decrypting + keys, as well as create new keys using it's desired configuration defaults. + + As an example:: + + >>> # your application can create a custom class when it initializes + >>> from passlib.totp import TOTP, generate_secret + >>> TotpFactory = TOTP.using(secrets={"1": generate_secret()}) + + >>> # subsequent TOTP objects created from this factory + >>> # will use the specified secrets to encrypt their keys... + >>> totp = TotpFactory.new() + >>> totp.to_dict() + {'enckey': {'c': 14, + 'k': 'H77SYXWORDPGVOQTFRR2HFUB3C45XXI7', + 's': 'G5DOQPIHIBUM2OOHHADQ', + 't': '1', + 'v': 1}, + 'type': 'totp', + 'v': 1} + + .. seealso:: :ref:`totp-creation` and :ref:`totp-storing-instances` tutorials for a usage example + """ + # XXX: could add support for setting default match 'window' and 'reuse' policy + + # :param now: + # Optional callable that should return current time for generator to use. + # Default to :func:`time.time`. This optional is generally not needed, + # and is mainly present for examples & unit-testing. + + subcls = type("TOTP", (cls,), {}) + + def norm_param(attr, value): + """ + helper which uses constructor to validate parameter value. + it returns corresponding attribute, so we use normalized value. + """ + # NOTE: this creates *subclass* instance, + # so normalization takes into account any custom params + # already stored. + kwds = dict(key=_DUMMY_KEY, format="raw") + kwds[attr] = value + obj = subcls(**kwds) + return getattr(obj, attr) + + if digits is not None: + subcls.digits = norm_param("digits", digits) + + if alg is not None: + subcls.alg = norm_param("alg", alg) + + if period is not None: + subcls.period = norm_param("period", period) + + # XXX: add default size as configurable parameter? + + if issuer is not None: + subcls.issuer = norm_param("issuer", issuer) + + if kwds: + subcls.wallet = AppWallet(**kwds) + if wallet: + raise TypeError("'wallet' and 'secrets' keywords are mutually exclusive") + elif wallet is not None: + if not isinstance(wallet, AppWallet): + raise exc.ExpectedTypeError(wallet, AppWallet, "wallet") + subcls.wallet = wallet + + if now is not None: + assert isinstance(now(), num_types) and now() >= 0, \ + "now() function must return non-negative int/float" + subcls.now = staticmethod(now) + + return subcls + + #============================================================================= + # init + #============================================================================= + + @classmethod + def new(cls, **kwds): + """ + convenience alias for creating new TOTP key, same as ``TOTP(new=True)`` + """ + return cls(new=True, **kwds) + + def __init__(self, key=None, format="base32", + # keyword only... + new=False, digits=None, alg=None, size=None, period=None, + label=None, issuer=None, changed=False, + **kwds): + super(TOTP, self).__init__(**kwds) + if changed: + self.changed = changed + + # validate & normalize alg + info = lookup_hash(alg or self.alg) + self.alg = info.name + digest_size = info.digest_size + if digest_size < 4: + raise RuntimeError("%r hash digest too small" % alg) + + # parse or generate new key + if new: + # generate new key + if key: + raise TypeError("'key' and 'new=True' are mutually exclusive") + if size is None: + # default to digest size, per RFC 6238 Section 5.1 + size = digest_size + elif size > digest_size: + # not forbidden by spec, but would just be wasted bytes. + # maybe just warn about this? + raise ValueError("'size' should be less than digest size " + "(%d)" % digest_size) + self.key = getrandbytes(rng, size) + elif not key: + raise TypeError("must specify either an existing 'key', or 'new=True'") + elif format == "encrypted": + # NOTE: this handles decrypting & setting '.key' + self.encrypted_key = key + elif key: + # use existing key, encoded using specified + self.key = _decode_bytes(key, format) + + # enforce min key size + if len(self.key) < self._min_key_size: + # only making this fatal for new=True, + # so that existing (but ridiculously small) keys can still be used. + msg = "for security purposes, secret key must be >= %d bytes" % self._min_key_size + if new: + raise ValueError(msg) + else: + warn(msg, exc.PasslibSecurityWarning, stacklevel=1) + + # validate digits + if digits is None: + digits = self.digits + if not isinstance(digits, int_types): + raise TypeError("digits must be an integer, not a %r" % type(digits)) + if digits < 6 or digits > 10: + raise ValueError("digits must in range(6,11)") + self.digits = digits + + # validate label + if label: + self._check_label(label) + self.label = label + + # validate issuer + if issuer: + self._check_issuer(issuer) + self.issuer = issuer + + # init period + if period is not None: + self._check_serial(period, "period", minval=1) + self.period = period + + #============================================================================= + # helpers to verify value types & ranges + #============================================================================= + + @staticmethod + def _check_serial(value, param, minval=0): + """ + check that serial value (e.g. 'counter') is non-negative integer + """ + if not isinstance(value, int_types): + raise exc.ExpectedTypeError(value, "int", param) + if value < minval: + raise ValueError("%s must be >= %d" % (param, minval)) + + @staticmethod + def _check_label(label): + """ + check that label doesn't contain chars forbidden by KeyURI spec + """ + if label and ":" in label: + raise ValueError("label may not contain ':'") + + @staticmethod + def _check_issuer(issuer): + """ + check that issuer doesn't contain chars forbidden by KeyURI spec + """ + if issuer and ":" in issuer: + raise ValueError("issuer may not contain ':'") + + #============================================================================= + # key attributes + #============================================================================= + + #------------------------------------------------------------------ + # raw key + #------------------------------------------------------------------ + @property + def key(self): + """ + secret key as raw bytes + """ + return self._key + + @key.setter + def key(self, value): + # set key + if not isinstance(value, bytes): + raise exc.ExpectedTypeError(value, bytes, "key") + self._key = value + + # clear cached properties derived from key + self._encrypted_key = self._keyed_hmac = None + + #------------------------------------------------------------------ + # encrypted key + #------------------------------------------------------------------ + @property + def encrypted_key(self): + """ + secret key, encrypted using application secret. + this match the output of :meth:`AppWallet.encrypt_key`, + and should be treated as an opaque json serializable object. + """ + enckey = self._encrypted_key + if enckey is None: + wallet = self.wallet + if not wallet: + raise TypeError("no application secrets present, can't encrypt TOTP key") + enckey = self._encrypted_key = wallet.encrypt_key(self.key) + return enckey + + @encrypted_key.setter + def encrypted_key(self, value): + wallet = self.wallet + if not wallet: + raise TypeError("no application secrets present, can't decrypt TOTP key") + self.key, needs_recrypt = wallet.decrypt_key(value) + if needs_recrypt: + # mark as changed so it gets re-encrypted & written to db + self.changed = True + else: + # cache encrypted key for re-use + self._encrypted_key = value + + #------------------------------------------------------------------ + # pretty-printed / encoded key helpers + #------------------------------------------------------------------ + + @property + def hex_key(self): + """ + secret key encoded as hexadecimal string + """ + return bascii_to_str(base64.b16encode(self.key)).lower() + + @property + def base32_key(self): + """ + secret key encoded as base32 string + """ + return b32encode(self.key) + + def pretty_key(self, format="base32", sep="-"): + """ + pretty-print the secret key. + + This is mainly useful for situations where the user cannot get the qrcode to work, + and must enter the key manually into their TOTP client. It tries to format + the key in a manner that is easier for humans to read. + + :param format: + format to output secret key. ``"hex"`` and ``"base32"`` are both accepted. + + :param sep: + separator to insert to break up key visually. + can be any of ``"-"`` (the default), ``" "``, or ``False`` (no separator). + + :return: + key as native string. + + Usage example:: + + >>> t = TOTP('s3jdvb7qd2r7jpxx') + >>> t.pretty_key() + 'S3JD-VB7Q-D2R7-JPXX' + """ + if format == "hex" or format == "base16": + key = self.hex_key + elif format == "base32": + key = self.base32_key + else: + raise ValueError("unknown byte-encoding format: %r" % (format,)) + if sep: + key = group_string(key, sep) + return key + + #============================================================================= + # time & token parsing + #============================================================================= + + @classmethod + def normalize_time(cls, time): + """ + Normalize time value to unix epoch seconds. + + :arg time: + Can be ``None``, :class:`!datetime`, + or unix epoch timestamp as :class:`!float` or :class:`!int`. + If ``None``, uses current system time. + Naive datetimes are treated as UTC. + + :returns: + unix epoch timestamp as :class:`int`. + """ + if isinstance(time, int_types): + return time + elif isinstance(time, float): + return int(time) + elif time is None: + return int(cls.now()) + elif hasattr(time, "utctimetuple"): + # coerce datetime to UTC timestamp + # NOTE: utctimetuple() assumes naive datetimes are in UTC + # NOTE: we explicitly *don't* want microseconds. + return calendar.timegm(time.utctimetuple()) + else: + raise exc.ExpectedTypeError(time, "int, float, or datetime", "time") + + def _time_to_counter(self, time): + """ + convert timestamp to HOTP counter using :attr:`period`. + """ + return time // self.period + + def _counter_to_time(self, counter): + """ + convert HOTP counter to timestamp using :attr:`period`. + """ + return counter * self.period + + @hybrid_method + def normalize_token(self_or_cls, token): + """ + Normalize OTP token representation: + strips whitespace, converts integers to a zero-padded string, + validates token content & number of digits. + + This is a hybrid method -- it can be called at the class level, + as ``TOTP.normalize_token()``, or the instance level as ``TOTP().normalize_token()``. + It will normalize to the instance-specific number of :attr:`~TOTP.digits`, + or use the class default. + + :arg token: + token as ascii bytes, unicode, or an integer. + + :raises ValueError: + if token has wrong number of digits, or contains non-numeric characters. + + :returns: + token as :class:`!unicode` string, containing only digits 0-9. + """ + digits = self_or_cls.digits + if isinstance(token, int_types): + token = u("%0*d") % (digits, token) + else: + token = to_unicode(token, param="token") + token = _clean_re.sub(u(""), token) + if not token.isdigit(): + raise MalformedTokenError("Token must contain only the digits 0-9") + if len(token) != digits: + raise MalformedTokenError("Token must have exactly %d digits" % digits) + return token + + #============================================================================= + # token generation + #============================================================================= + +# # debug helper +# def generate_range(self, size, time=None): +# counter = self._time_to_counter(time) - (size + 1) // 2 +# end = counter + size +# while counter <= end: +# token = self._generate(counter) +# yield TotpToken(self, token, counter) +# counter += 1 + + def generate(self, time=None): + """ + Generate token for specified time + (uses current time if none specified). + + :arg time: + Can be ``None``, a :class:`!datetime`, + or class:`!float` / :class:`!int` unix epoch timestamp. + If ``None`` (the default), uses current system time. + Naive datetimes are treated as UTC. + + :returns: + + A :class:`TotpToken` instance, which can be treated + as a sequence of ``(token, expire_time)`` -- see that class + for more details. + + Usage example:: + + >>> # generate a new token, wrapped in a TotpToken instance... + >>> otp = TOTP('s3jdvb7qd2r7jpxx') + >>> otp.generate(1419622739) + + + >>> # when you just need the token... + >>> otp.generate(1419622739).token + '897212' + """ + time = self.normalize_time(time) + counter = self._time_to_counter(time) + if counter < 0: + raise ValueError("timestamp must be >= 0") + token = self._generate(counter) + return TotpToken(self, token, counter) + + def _generate(self, counter): + """ + base implementation of HOTP token generation algorithm. + + :arg counter: HOTP counter, as non-negative integer + :returns: token as unicode string + """ + # generate digest + assert isinstance(counter, int_types), "counter must be integer" + assert counter >= 0, "counter must be non-negative" + keyed_hmac = self._keyed_hmac + if keyed_hmac is None: + keyed_hmac = self._keyed_hmac = compile_hmac(self.alg, self.key) + digest = keyed_hmac(_pack_uint64(counter)) + digest_size = keyed_hmac.digest_info.digest_size + assert len(digest) == digest_size, "digest_size: sanity check failed" + + # derive 31-bit token value + assert digest_size >= 20, "digest_size: sanity check 2 failed" # otherwise 0xF+4 will run off end of hash. + offset = byte_elem_value(digest[-1]) & 0xF + value = _unpack_uint32(digest[offset:offset+4])[0] & 0x7fffffff + + # render to decimal string, return last chars + # NOTE: the 10'th digit is not as secure, as it can only take on values 0-2, not 0-9, + # due to 31-bit mask on int ">I". But some servers / clients use it :| + # if 31-bit mask removed (which breaks spec), would only get values 0-4. + digits = self.digits + assert 0 < digits < 11, "digits: sanity check failed" + return (u("%0*d") % (digits, value))[-digits:] + + #============================================================================= + # token verification + #============================================================================= + + @classmethod + def verify(cls, token, source, **kwds): + r""" + Convenience wrapper around :meth:`TOTP.from_source` and :meth:`TOTP.match`. + + This parses a TOTP key & configuration from the specified source, + and tries and match the token. + It's designed to parallel the :meth:`passlib.ifc.PasswordHash.verify` method. + + :param token: + Token string to match. + + :param source: + Serialized TOTP key. + Can be anything accepted by :meth:`TOTP.from_source`. + + :param \\*\\*kwds: + All additional keywords passed to :meth:`TOTP.match`. + + :return: + A :class:`TotpMatch` instance, or raises a :exc:`TokenError`. + """ + return cls.from_source(source).match(token, **kwds) + + def match(self, token, time=None, window=30, skew=0, last_counter=None): + """ + Match TOTP token against specified timestamp. + Searches within a window before & after the provided time, + in order to account for transmission delay and small amounts of skew in the client's clock. + + :arg token: + Token to validate. + may be integer or string (whitespace and hyphens are ignored). + + :param time: + Unix epoch timestamp, can be any of :class:`!float`, :class:`!int`, or :class:`!datetime`. + if ``None`` (the default), uses current system time. + *this should correspond to the time the token was received from the client*. + + :param int window: + How far backward and forward in time to search for a match. + Measured in seconds. Defaults to ``30``. Typically only useful if set + to multiples of :attr:`period`. + + :param int skew: + Adjust timestamp by specified value, to account for excessive + client clock skew. Measured in seconds. Defaults to ``0``. + + Negative skew (the common case) indicates transmission delay, + and/or that the client clock is running behind the server. + + Positive skew indicates the client clock is running ahead of the server + (and by enough that it cancels out any negative skew added by + the transmission delay). + + You should ensure the server clock uses a reliable time source such as NTP, + so that only the client clock's inaccuracy needs to be accounted for. + + This is an advanced parameter that should usually be left at ``0``; + The **window** parameter is usually enough to account + for any observed transmission delay. + + :param last_counter: + Optional value of last counter value that was successfully used. + If specified, verify will never search earlier counters, + no matter how large the window is. + + Useful when client has previously authenticated, + and thus should never provide a token older than previously + verified value. + + :raises ~passlib.exc.TokenError: + + If the token is malformed, fails to match, or has already been used. + + :returns TotpMatch: + + Returns a :class:`TotpMatch` instance on successful match. + Can be treated as tuple of ``(counter, time)``. + Raises error if token is malformed / can't be verified. + + Usage example:: + + >>> totp = TOTP('s3jdvb7qd2r7jpxx') + + >>> # valid token for this time period + >>> totp.match('897212', 1419622729) + + + >>> # token from counter step 30 sec ago (within allowed window) + >>> totp.match('000492', 1419622729) + + + >>> # invalid token -- token from 60 sec ago (outside of window) + >>> totp.match('760389', 1419622729) + Traceback: + ... + InvalidTokenError: Token did not match + """ + time = self.normalize_time(time) + self._check_serial(window, "window") + + client_time = time + skew + if last_counter is None: + last_counter = -1 + start = max(last_counter, self._time_to_counter(client_time - window)) + end = self._time_to_counter(client_time + window) + 1 + # XXX: could pass 'expected = _time_to_counter(client_time + TRANSMISSION_DELAY)' + # to the _find_match() method, would help if window set to very large value. + + counter = self._find_match(token, start, end) + assert counter >= last_counter, "sanity check failed: counter went backward" + + if counter == last_counter: + raise UsedTokenError(expire_time=(last_counter + 1) * self.period) + + # NOTE: By returning match tied to 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 H5%B*8?4>f| literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/utils/__pycache__/binary.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/utils/__pycache__/binary.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..27af1b7405c528da4b58a5dea810c1a796baec47 GIT binary patch literal 33081 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 literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/utils/__pycache__/handlers.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/utils/__pycache__/handlers.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..265fce8450ff4f0af8e6697fd7131c9f6b7f54de GIT binary patch literal 100192 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 W_k$WNoTiWB7UbiWf~ACF{J#Ou%;xz3 literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/utils/__pycache__/md4.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/utils/__pycache__/md4.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4e660cbf96e1b2ee4ccc6df35f46860c2fc4ff64 GIT binary patch literal 897 zcmZ`%zi$&U6t?d!mk_9+m5^En^i&C@kY(}D}MohL6I1$h>eLYs7#%Bmo%jeJlW4@`?KHoedmu;Q$>RDq5HY}#UkWK zHkky@M{(N1;XPrbP8el|q=8X4C@}~(n90pUgOE?y$3Y3Ry0mT~GADC^UAIR_n^}hx zaoET4IzRcU7hc)Sen!gqgYU)2(^`s_lO{rHCt%C42-VuwcJ1lo=d~)VZ&qKd)he*F zvbF|KYOq~f-+YPdD;sdnxmzr*SC>KiDwPaE)q@rXsnjlKkVcUBu}Zr>G@Fwm?RY;* zl*2@q>%_^BY1HsN?Jvw@<@vJHR-sOs%}TN7i?$C@to8)s8q!d71m5az$ind^mGv?O=YZ#UXcd==hzmX=&C622ID z?OhLlrh8t+9U1B(e<#{MD?A+g{V2YUcV{nAzri##DWyXrN39{r(hQ-uj>+8Rc0#N^ z?izNL4sBx3{>qzl?l-|S9HkrN+VtB}KR+xG>Kv2B%k9LT?(e?4^VOdHmbKe`^TfK| LU+u5PQ&{{Dvr!Bi literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/utils/__pycache__/pbkdf2.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/utils/__pycache__/pbkdf2.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..592080075f35a60e2d5e72b037afe7f12a5646e2 GIT binary patch literal 6779 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 literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/utils/binary.py b/ansible/lib/python3.11/site-packages/passlib/utils/binary.py new file mode 100644 index 000000000..521b64a38 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/utils/binary.py @@ -0,0 +1,884 @@ +""" +passlib.utils.binary - binary data encoding/decoding/manipulation +""" +#============================================================================= +# imports +#============================================================================= +# core +from __future__ import absolute_import, division, print_function +from base64 import ( + b64encode, + b64decode, + b32decode as _b32decode, + b32encode as _b32encode, +) +from binascii import b2a_base64, a2b_base64, Error as _BinAsciiError +import logging +log = logging.getLogger(__name__) +# site +# pkg +from passlib import exc +from passlib.utils.compat import ( + PY3, bascii_to_str, + irange, imap, iter_byte_chars, join_byte_values, join_byte_elems, + nextgetter, suppress_cause, + u, unicode, unicode_or_bytes_types, +) +from passlib.utils.decor import memoized_property +# from passlib.utils import BASE64_CHARS, HASH64_CHARS +# local +__all__ = [ + # constants + "BASE64_CHARS", "PADDED_BASE64_CHARS", + "AB64_CHARS", + "HASH64_CHARS", + "BCRYPT_CHARS", + "HEX_CHARS", "LOWER_HEX_CHARS", "UPPER_HEX_CHARS", + + "ALL_BYTE_VALUES", + + # misc + "compile_byte_translation", + + # base64 + 'ab64_encode', 'ab64_decode', + 'b64s_encode', 'b64s_decode', + + # base32 + "b32encode", "b32decode", + + # custom encodings + 'Base64Engine', + 'LazyBase64Engine', + 'h64', + 'h64big', + 'bcrypt64', +] + +#============================================================================= +# constant strings +#============================================================================= + +#------------------------------------------------------------- +# common salt_chars & checksum_chars values +#------------------------------------------------------------- + +#: standard base64 charmap +BASE64_CHARS = u("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/") + +#: alt base64 charmap -- "." instead of "+" +AB64_CHARS = u("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789./") + +#: charmap used by HASH64 encoding. +HASH64_CHARS = u("./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz") + +#: charmap used by BCrypt +BCRYPT_CHARS = u("./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") + +#: std base64 chars + padding char +PADDED_BASE64_CHARS = BASE64_CHARS + u("=") + +#: all hex chars +HEX_CHARS = u("0123456789abcdefABCDEF") + +#: upper case hex chars +UPPER_HEX_CHARS = u("0123456789ABCDEF") + +#: lower case hex chars +LOWER_HEX_CHARS = u("0123456789abcdef") + +#------------------------------------------------------------- +# byte strings +#------------------------------------------------------------- + +#: special byte string containing all possible byte values +#: NOTE: for efficiency, this is treated as singleton by some of the code +ALL_BYTE_VALUES = join_byte_values(irange(256)) + +#: some string constants we reuse +B_EMPTY = b'' +B_NULL = b'\x00' +B_EQUAL = b'=' + +#============================================================================= +# byte translation +#============================================================================= + +#: base list used to compile byte translations +_TRANSLATE_SOURCE = list(iter_byte_chars(ALL_BYTE_VALUES)) + +def compile_byte_translation(mapping, source=None): + """ + return a 256-byte string for translating bytes using specified mapping. + bytes not specified by mapping will be left alone. + + :param mapping: + dict mapping input byte (str or int) -> output byte (str or int). + + :param source: + optional existing byte translation string to use as base. + (must be 255-length byte string). defaults to identity mapping. + + :returns: + 255-length byte string for passing to bytes().translate. + """ + if source is None: + target = _TRANSLATE_SOURCE[:] + else: + assert isinstance(source, bytes) and len(source) == 255 + target = list(iter_byte_chars(source)) + for k, v in mapping.items(): + if isinstance(k, unicode_or_bytes_types): + k = ord(k) + assert isinstance(k, int) and 0 <= k < 256 + if isinstance(v, unicode): + v = v.encode("ascii") + assert isinstance(v, bytes) and len(v) == 1 + target[k] = v + return B_EMPTY.join(target) + +#============================================================================= +# unpadding / stripped base64 encoding +#============================================================================= +def b64s_encode(data): + """ + encode using shortened base64 format which omits padding & whitespace. + uses default ``+/`` altchars. + """ + return b2a_base64(data).rstrip(_BASE64_STRIP) + +def b64s_decode(data): + """ + decode from shortened base64 format which omits padding & whitespace. + uses default ``+/`` altchars. + """ + if isinstance(data, unicode): + # needs bytes for replace() call, but want to accept ascii-unicode ala a2b_base64() + try: + data = data.encode("ascii") + except UnicodeEncodeError: + raise suppress_cause(ValueError("string argument should contain only ASCII characters")) + off = len(data) & 3 + if off == 0: + pass + elif off == 2: + data += _BASE64_PAD2 + elif off == 3: + data += _BASE64_PAD1 + else: # off == 1 + raise ValueError("invalid base64 input") + try: + return a2b_base64(data) + except _BinAsciiError as err: + raise suppress_cause(TypeError(err)) + +#============================================================================= +# adapted-base64 encoding +#============================================================================= +_BASE64_STRIP = b"=\n" +_BASE64_PAD1 = b"=" +_BASE64_PAD2 = b"==" + +# XXX: Passlib 1.8/1.9 -- deprecate everything that's using ab64_encode(), +# have it start outputing b64s_encode() instead? can use a64_decode() to retain backwards compat. + +def ab64_encode(data): + """ + encode using shortened base64 format which omits padding & whitespace. + uses custom ``./`` altchars. + + it is primarily used by Passlib's custom pbkdf2 hashes. + """ + return b64s_encode(data).replace(b"+", b".") + +def ab64_decode(data): + """ + decode from shortened base64 format which omits padding & whitespace. + uses custom ``./`` altchars, but supports decoding normal ``+/`` altchars as well. + + it is primarily used by Passlib's custom pbkdf2 hashes. + """ + if isinstance(data, unicode): + # needs bytes for replace() call, but want to accept ascii-unicode ala a2b_base64() + try: + data = data.encode("ascii") + except UnicodeEncodeError: + raise suppress_cause(ValueError("string argument should contain only ASCII characters")) + return b64s_decode(data.replace(b".", b"+")) + +#============================================================================= +# base32 codec +#============================================================================= + +def b32encode(source): + """ + wrapper around :func:`base64.b32encode` which strips padding, + and returns a native string. + """ + # NOTE: using upper case by default here, since 'I & L' are less + # visually ambiguous than 'i & l' + return bascii_to_str(_b32encode(source).rstrip(B_EQUAL)) + +#: byte translation map to replace common mistyped base32 chars. +#: XXX: could correct '1' -> 'I', but could be a mistyped lower-case 'l', so leaving it alone. +_b32_translate = compile_byte_translation({"8": "B", "0": "O"}) + +#: helper to add padding +_b32_decode_pad = B_EQUAL * 8 + +def b32decode(source): + """ + wrapper around :func:`base64.b32decode` + which handles common mistyped chars. + padding optional, ignored if present. + """ + # encode & correct for typos + if isinstance(source, unicode): + source = source.encode("ascii") + source = source.translate(_b32_translate) + + # pad things so final string is multiple of 8 + remainder = len(source) & 0x7 + if remainder: + source += _b32_decode_pad[:-remainder] + + # XXX: py27 stdlib's version of this has some inefficiencies, + # could look into using optimized version. + return _b32decode(source, True) + +#============================================================================= +# base64-variant encoding +#============================================================================= + +class Base64Engine(object): + """Provides routines for encoding/decoding base64 data using + arbitrary character mappings, selectable endianness, etc. + + :arg charmap: + A string of 64 unique characters, + which will be used to encode successive 6-bit chunks of data. + A character's position within the string should correspond + to its 6-bit value. + + :param big: + Whether the encoding should be big-endian (default False). + + .. note:: + This class does not currently handle base64's padding characters + in any way what so ever. + + Raw Bytes <-> Encoded Bytes + =========================== + The following methods convert between raw bytes, + and strings encoded using the engine's specific base64 variant: + + .. automethod:: encode_bytes + .. automethod:: decode_bytes + .. automethod:: encode_transposed_bytes + .. automethod:: decode_transposed_bytes + + .. + .. automethod:: check_repair_unused + .. automethod:: repair_unused + + Integers <-> Encoded Bytes + ========================== + The following methods allow encoding and decoding + unsigned integers to and from the engine's specific base64 variant. + Endianess is determined by the engine's ``big`` constructor keyword. + + .. automethod:: encode_int6 + .. automethod:: decode_int6 + + .. automethod:: encode_int12 + .. automethod:: decode_int12 + + .. automethod:: encode_int24 + .. automethod:: decode_int24 + + .. automethod:: encode_int64 + .. automethod:: decode_int64 + + Informational Attributes + ======================== + .. attribute:: charmap + + unicode string containing list of characters used in encoding; + position in string matches 6bit value of character. + + .. attribute:: bytemap + + bytes version of :attr:`charmap` + + .. attribute:: big + + boolean flag indicating this using big-endian encoding. + """ + + #=================================================================== + # instance attrs + #=================================================================== + # public config + bytemap = None # charmap as bytes + big = None # little or big endian + + # filled in by init based on charmap. + # (byte elem: single byte under py2, 8bit int under py3) + _encode64 = None # maps 6bit value -> byte elem + _decode64 = None # maps byte elem -> 6bit value + + # helpers filled in by init based on endianness + _encode_bytes = None # throws IndexError if bad value (shouldn't happen) + _decode_bytes = None # throws KeyError if bad char. + + #=================================================================== + # init + #=================================================================== + def __init__(self, charmap, big=False): + # validate charmap, generate encode64/decode64 helper functions. + if isinstance(charmap, unicode): + charmap = charmap.encode("latin-1") + elif not isinstance(charmap, bytes): + raise exc.ExpectedStringError(charmap, "charmap") + if len(charmap) != 64: + raise ValueError("charmap must be 64 characters in length") + if len(set(charmap)) != 64: + raise ValueError("charmap must not contain duplicate characters") + self.bytemap = charmap + self._encode64 = charmap.__getitem__ + lookup = dict((value, idx) for idx, value in enumerate(charmap)) + self._decode64 = lookup.__getitem__ + + # validate big, set appropriate helper functions. + self.big = big + if big: + self._encode_bytes = self._encode_bytes_big + self._decode_bytes = self._decode_bytes_big + else: + self._encode_bytes = self._encode_bytes_little + self._decode_bytes = self._decode_bytes_little + + # TODO: support padding character + ##if padding is not None: + ## if isinstance(padding, unicode): + ## padding = padding.encode("latin-1") + ## elif not isinstance(padding, bytes): + ## raise TypeError("padding char must be unicode or bytes") + ## if len(padding) != 1: + ## raise ValueError("padding must be single character") + ##self.padding = padding + + @property + def charmap(self): + """charmap as unicode""" + return self.bytemap.decode("latin-1") + + #=================================================================== + # encoding byte strings + #=================================================================== + def encode_bytes(self, source): + """encode bytes to base64 string. + + :arg source: byte string to encode. + :returns: byte string containing encoded data. + """ + if not isinstance(source, bytes): + raise TypeError("source must be bytes, not %s" % (type(source),)) + chunks, tail = divmod(len(source), 3) + if PY3: + next_value = nextgetter(iter(source)) + else: + next_value = nextgetter(ord(elem) for elem in source) + gen = self._encode_bytes(next_value, chunks, tail) + out = join_byte_elems(imap(self._encode64, gen)) + ##if tail: + ## padding = self.padding + ## if padding: + ## out += padding * (3-tail) + return out + + def _encode_bytes_little(self, next_value, chunks, tail): + """helper used by encode_bytes() to handle little-endian encoding""" + # + # output bit layout: + # + # first byte: v1 543210 + # + # second byte: v1 ....76 + # +v2 3210.. + # + # third byte: v2 ..7654 + # +v3 10.... + # + # fourth byte: v3 765432 + # + idx = 0 + while idx < chunks: + v1 = next_value() + v2 = next_value() + v3 = next_value() + yield v1 & 0x3f + yield ((v2 & 0x0f)<<2)|(v1>>6) + yield ((v3 & 0x03)<<4)|(v2>>4) + yield v3>>2 + idx += 1 + if tail: + v1 = next_value() + if tail == 1: + # note: 4 msb of last byte are padding + yield v1 & 0x3f + yield v1>>6 + else: + assert tail == 2 + # note: 2 msb of last byte are padding + v2 = next_value() + yield v1 & 0x3f + yield ((v2 & 0x0f)<<2)|(v1>>6) + yield v2>>4 + + def _encode_bytes_big(self, next_value, chunks, tail): + """helper used by encode_bytes() to handle big-endian encoding""" + # + # output bit layout: + # + # first byte: v1 765432 + # + # second byte: v1 10.... + # +v2 ..7654 + # + # third byte: v2 3210.. + # +v3 ....76 + # + # fourth byte: v3 543210 + # + idx = 0 + while idx < chunks: + v1 = next_value() + v2 = next_value() + v3 = next_value() + yield v1>>2 + yield ((v1&0x03)<<4)|(v2>>4) + yield ((v2&0x0f)<<2)|(v3>>6) + yield v3 & 0x3f + idx += 1 + if tail: + v1 = next_value() + if tail == 1: + # note: 4 lsb of last byte are padding + yield v1>>2 + yield (v1&0x03)<<4 + else: + assert tail == 2 + # note: 2 lsb of last byte are padding + v2 = next_value() + yield v1>>2 + yield ((v1&0x03)<<4)|(v2>>4) + yield ((v2&0x0f)<<2) + + #=================================================================== + # decoding byte strings + #=================================================================== + + def decode_bytes(self, source): + """decode bytes from base64 string. + + :arg source: byte string to decode. + :returns: byte string containing decoded data. + """ + if not isinstance(source, bytes): + raise TypeError("source must be bytes, not %s" % (type(source),)) + ##padding = self.padding + ##if padding: + ## # TODO: add padding size check? + ## source = source.rstrip(padding) + chunks, tail = divmod(len(source), 4) + if tail == 1: + # only 6 bits left, can't encode a whole byte! + raise ValueError("input string length cannot be == 1 mod 4") + next_value = nextgetter(imap(self._decode64, source)) + try: + return join_byte_values(self._decode_bytes(next_value, chunks, tail)) + except KeyError as err: + raise ValueError("invalid character: %r" % (err.args[0],)) + + def _decode_bytes_little(self, next_value, chunks, tail): + """helper used by decode_bytes() to handle little-endian encoding""" + # + # input bit layout: + # + # first byte: v1 ..543210 + # +v2 10...... + # + # second byte: v2 ....5432 + # +v3 3210.... + # + # third byte: v3 ......54 + # +v4 543210.. + # + idx = 0 + while idx < chunks: + v1 = next_value() + v2 = next_value() + v3 = next_value() + v4 = next_value() + yield v1 | ((v2 & 0x3) << 6) + yield (v2>>2) | ((v3 & 0xF) << 4) + yield (v3>>4) | (v4<<2) + idx += 1 + if tail: + # tail is 2 or 3 + v1 = next_value() + v2 = next_value() + yield v1 | ((v2 & 0x3) << 6) + # NOTE: if tail == 2, 4 msb of v2 are ignored (should be 0) + if tail == 3: + # NOTE: 2 msb of v3 are ignored (should be 0) + v3 = next_value() + yield (v2>>2) | ((v3 & 0xF) << 4) + + def _decode_bytes_big(self, next_value, chunks, tail): + """helper used by decode_bytes() to handle big-endian encoding""" + # + # input bit layout: + # + # first byte: v1 543210.. + # +v2 ......54 + # + # second byte: v2 3210.... + # +v3 ....5432 + # + # third byte: v3 10...... + # +v4 ..543210 + # + idx = 0 + while idx < chunks: + v1 = next_value() + v2 = next_value() + v3 = next_value() + v4 = next_value() + yield (v1<<2) | (v2>>4) + yield ((v2&0xF)<<4) | (v3>>2) + yield ((v3&0x3)<<6) | v4 + idx += 1 + if tail: + # tail is 2 or 3 + v1 = next_value() + v2 = next_value() + yield (v1<<2) | (v2>>4) + # NOTE: if tail == 2, 4 lsb of v2 are ignored (should be 0) + if tail == 3: + # NOTE: 2 lsb of v3 are ignored (should be 0) + v3 = next_value() + yield ((v2&0xF)<<4) | (v3>>2) + + #=================================================================== + # encode/decode helpers + #=================================================================== + + # padmap2/3 - dict mapping last char of string -> + # equivalent char with no padding bits set. + + def __make_padset(self, bits): + """helper to generate set of valid last chars & bytes""" + pset = set(c for i,c in enumerate(self.bytemap) if not i & bits) + pset.update(c for i,c in enumerate(self.charmap) if not i & bits) + return frozenset(pset) + + @memoized_property + def _padinfo2(self): + """mask to clear padding bits, and valid last bytes (for strings 2 % 4)""" + # 4 bits of last char unused (lsb for big, msb for little) + bits = 15 if self.big else (15<<2) + return ~bits, self.__make_padset(bits) + + @memoized_property + def _padinfo3(self): + """mask to clear padding bits, and valid last bytes (for strings 3 % 4)""" + # 2 bits of last char unused (lsb for big, msb for little) + bits = 3 if self.big else (3<<4) + return ~bits, self.__make_padset(bits) + + def check_repair_unused(self, source): + """helper to detect & clear invalid unused bits in last character. + + :arg source: + encoded data (as ascii bytes or unicode). + + :returns: + `(True, result)` if the string was repaired, + `(False, source)` if the string was ok as-is. + """ + # figure out how many padding bits there are in last char. + tail = len(source) & 3 + if tail == 2: + mask, padset = self._padinfo2 + elif tail == 3: + mask, padset = self._padinfo3 + elif not tail: + return False, source + else: + raise ValueError("source length must != 1 mod 4") + + # check if last char is ok (padset contains bytes & unicode versions) + last = source[-1] + if last in padset: + return False, source + + # we have dirty bits - repair the string by decoding last char, + # clearing the padding bits via , and encoding new char. + if isinstance(source, unicode): + cm = self.charmap + last = cm[cm.index(last) & mask] + assert last in padset, "failed to generate valid padding char" + else: + # NOTE: this assumes ascii-compat encoding, and that + # all chars used by encoding are 7-bit ascii. + last = self._encode64(self._decode64(last) & mask) + assert last in padset, "failed to generate valid padding char" + if PY3: + last = bytes([last]) + return True, source[:-1] + last + + def repair_unused(self, source): + return self.check_repair_unused(source)[1] + + ##def transcode(self, source, other): + ## return ''.join( + ## other.charmap[self.charmap.index(char)] + ## for char in source + ## ) + + ##def random_encoded_bytes(self, size, random=None, unicode=False): + ## "return random encoded string of given size" + ## data = getrandstr(random or rng, + ## self.charmap if unicode else self.bytemap, size) + ## return self.repair_unused(data) + + #=================================================================== + # transposed encoding/decoding + #=================================================================== + def encode_transposed_bytes(self, source, offsets): + """encode byte string, first transposing source using offset list""" + if not isinstance(source, bytes): + raise TypeError("source must be bytes, not %s" % (type(source),)) + tmp = join_byte_elems(source[off] for off in offsets) + return self.encode_bytes(tmp) + + def decode_transposed_bytes(self, source, offsets): + """decode byte string, then reverse transposition described by offset list""" + # NOTE: if transposition does not use all bytes of source, + # the original can't be recovered... and join_byte_elems() will throw + # an error because 1+ values in will be None. + tmp = self.decode_bytes(source) + buf = [None] * len(offsets) + for off, char in zip(offsets, tmp): + buf[off] = char + return join_byte_elems(buf) + + #=================================================================== + # integer decoding helpers - mainly used by des_crypt family + #=================================================================== + def _decode_int(self, source, bits): + """decode base64 string -> integer + + :arg source: base64 string to decode. + :arg bits: number of bits in resulting integer. + + :raises ValueError: + * if the string contains invalid base64 characters. + * if the string is not long enough - it must be at least + ``int(ceil(bits/6))`` in length. + + :returns: + a integer in the range ``0 <= n < 2**bits`` + """ + if not isinstance(source, bytes): + raise TypeError("source must be bytes, not %s" % (type(source),)) + big = self.big + pad = -bits % 6 + chars = (bits+pad)/6 + if len(source) != chars: + raise ValueError("source must be %d chars" % (chars,)) + decode = self._decode64 + out = 0 + try: + for c in source if big else reversed(source): + out = (out<<6) + decode(c) + except KeyError: + raise ValueError("invalid character in string: %r" % (c,)) + if pad: + # strip padding bits + if big: + out >>= pad + else: + out &= (1< 6 bit integer""" + if not isinstance(source, bytes): + raise TypeError("source must be bytes, not %s" % (type(source),)) + if len(source) != 1: + raise ValueError("source must be exactly 1 byte") + if PY3: + # convert to 8bit int before doing lookup + source = source[0] + try: + return self._decode64(source) + except KeyError: + raise ValueError("invalid character") + + def decode_int12(self, source): + """decodes 2 char string -> 12-bit integer""" + if not isinstance(source, bytes): + raise TypeError("source must be bytes, not %s" % (type(source),)) + if len(source) != 2: + raise ValueError("source must be exactly 2 bytes") + decode = self._decode64 + try: + if self.big: + return decode(source[1]) + (decode(source[0])<<6) + else: + return decode(source[0]) + (decode(source[1])<<6) + except KeyError: + raise ValueError("invalid character") + + def decode_int24(self, source): + """decodes 4 char string -> 24-bit integer""" + if not isinstance(source, bytes): + raise TypeError("source must be bytes, not %s" % (type(source),)) + if len(source) != 4: + raise ValueError("source must be exactly 4 bytes") + decode = self._decode64 + try: + if self.big: + return decode(source[3]) + (decode(source[2])<<6)+ \ + (decode(source[1])<<12) + (decode(source[0])<<18) + else: + return decode(source[0]) + (decode(source[1])<<6)+ \ + (decode(source[2])<<12) + (decode(source[3])<<18) + except KeyError: + raise ValueError("invalid character") + + def decode_int30(self, source): + """decode 5 char string -> 30 bit integer""" + return self._decode_int(source, 30) + + def decode_int64(self, source): + """decode 11 char base64 string -> 64-bit integer + + this format is used primarily by des-crypt & variants to encode + the DES output value used as a checksum. + """ + return self._decode_int(source, 64) + + #=================================================================== + # integer encoding helpers - mainly used by des_crypt family + #=================================================================== + def _encode_int(self, value, bits): + """encode integer into base64 format + + :arg value: non-negative integer to encode + :arg bits: number of bits to encode + + :returns: + a string of length ``int(ceil(bits/6.0))``. + """ + assert value >= 0, "caller did not sanitize input" + pad = -bits % 6 + bits += pad + if self.big: + itr = irange(bits-6, -6, -6) + # shift to add lsb padding. + value <<= pad + else: + itr = irange(0, bits, 6) + # padding is msb, so no change needed. + return join_byte_elems(imap(self._encode64, + ((value>>off) & 0x3f for off in itr))) + + #--------------------------------------------------------------- + # optimized versions for common integer sizes + #--------------------------------------------------------------- + + def encode_int6(self, value): + """encodes 6-bit integer -> single hash64 character""" + if value < 0 or value > 63: + raise ValueError("value out of range") + if PY3: + return self.bytemap[value:value+1] + else: + return self._encode64(value) + + def encode_int12(self, value): + """encodes 12-bit integer -> 2 char string""" + if value < 0 or value > 0xFFF: + raise ValueError("value out of range") + raw = [value & 0x3f, (value>>6) & 0x3f] + if self.big: + raw = reversed(raw) + return join_byte_elems(imap(self._encode64, raw)) + + def encode_int24(self, value): + """encodes 24-bit integer -> 4 char string""" + if value < 0 or value > 0xFFFFFF: + raise ValueError("value out of range") + raw = [value & 0x3f, (value>>6) & 0x3f, + (value>>12) & 0x3f, (value>>18) & 0x3f] + if self.big: + raw = reversed(raw) + return join_byte_elems(imap(self._encode64, raw)) + + def encode_int30(self, value): + """decode 5 char string -> 30 bit integer""" + if value < 0 or value > 0x3fffffff: + raise ValueError("value out of range") + return self._encode_int(value, 30) + + def encode_int64(self, value): + """encode 64-bit integer -> 11 char hash64 string + + this format is used primarily by des-crypt & variants to encode + the DES output value used as a checksum. + """ + if value < 0 or value > 0xffffffffffffffff: + raise ValueError("value out of range") + return self._encode_int(value, 64) + + #=================================================================== + # eof + #=================================================================== + +class LazyBase64Engine(Base64Engine): + """Base64Engine which delays initialization until it's accessed""" + _lazy_opts = None + + def __init__(self, *args, **kwds): + self._lazy_opts = (args, kwds) + + def _lazy_init(self): + args, kwds = self._lazy_opts + super(LazyBase64Engine, self).__init__(*args, **kwds) + del self._lazy_opts + self.__class__ = Base64Engine + + def __getattribute__(self, attr): + if not attr.startswith("_"): + self._lazy_init() + return object.__getattribute__(self, attr) + +#------------------------------------------------------------- +# common variants +#------------------------------------------------------------- + +h64 = LazyBase64Engine(HASH64_CHARS) +h64big = LazyBase64Engine(HASH64_CHARS, big=True) +bcrypt64 = LazyBase64Engine(BCRYPT_CHARS, big=True) + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/utils/compat/__init__.py b/ansible/lib/python3.11/site-packages/passlib/utils/compat/__init__.py new file mode 100644 index 000000000..f6ead2436 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/utils/compat/__init__.py @@ -0,0 +1,474 @@ +"""passlib.utils.compat - python 2/3 compatibility helpers""" +#============================================================================= +# figure out what we're running +#============================================================================= + +#------------------------------------------------------------------------ +# python version +#------------------------------------------------------------------------ +import sys +PY2 = sys.version_info < (3,0) +PY3 = sys.version_info >= (3,0) + +# make sure it's not an unsupported version, even if we somehow got this far +if sys.version_info < (2,6) or (3,0) <= sys.version_info < (3,2): + raise RuntimeError("Passlib requires Python 2.6, 2.7, or >= 3.2 (as of passlib 1.7)") + +PY26 = sys.version_info < (2,7) + +#------------------------------------------------------------------------ +# python implementation +#------------------------------------------------------------------------ +JYTHON = sys.platform.startswith('java') + +PYPY = hasattr(sys, "pypy_version_info") + +if PYPY and sys.pypy_version_info < (2,0): + raise RuntimeError("passlib requires pypy >= 2.0 (as of passlib 1.7)") + +# e.g. '2.7.7\n[Pyston 0.5.1]' +# NOTE: deprecated support 2019-11 +PYSTON = "Pyston" in sys.version + +#============================================================================= +# common imports +#============================================================================= +import logging; log = logging.getLogger(__name__) +if PY3: + import builtins +else: + import __builtin__ as builtins + +def add_doc(obj, doc): + """add docstring to an object""" + obj.__doc__ = doc + +#============================================================================= +# the default exported vars +#============================================================================= +__all__ = [ + # python versions + 'PY2', 'PY3', 'PY26', + + # io + 'BytesIO', 'StringIO', 'NativeStringIO', 'SafeConfigParser', + 'print_', + + # type detection +## 'is_mapping', + 'int_types', + 'num_types', + 'unicode_or_bytes_types', + 'native_string_types', + + # unicode/bytes types & helpers + 'u', + 'unicode', + 'uascii_to_str', 'bascii_to_str', + 'str_to_uascii', 'str_to_bascii', + 'join_unicode', 'join_bytes', + 'join_byte_values', 'join_byte_elems', + 'byte_elem_value', + 'iter_byte_values', + + # iteration helpers + 'irange', #'lrange', + 'imap', 'lmap', + 'iteritems', 'itervalues', + 'next', + + # collections + 'OrderedDict', + + # context helpers + 'nullcontext', + + # introspection + 'get_method_function', 'add_doc', +] + +# begin accumulating mapping of lazy-loaded attrs, +# 'merged' into module at bottom +_lazy_attrs = dict() + +#============================================================================= +# unicode & bytes types +#============================================================================= +if PY3: + unicode = str + + # TODO: once we drop python 3.2 support, can use u'' again! + def u(s): + assert isinstance(s, str) + return s + + unicode_or_bytes_types = (str, bytes) + native_string_types = (unicode,) + +else: + unicode = builtins.unicode + + def u(s): + assert isinstance(s, str) + return s.decode("unicode_escape") + + unicode_or_bytes_types = (basestring,) + native_string_types = (basestring,) + +# shorter preferred aliases +unicode_or_bytes = unicode_or_bytes_types +unicode_or_str = native_string_types + +# unicode -- unicode type, regardless of python version +# bytes -- bytes type, regardless of python version +# unicode_or_bytes_types -- types that text can occur in, whether encoded or not +# native_string_types -- types that native python strings (dict keys etc) can occur in. + +#============================================================================= +# unicode & bytes helpers +#============================================================================= +# function to join list of unicode strings +join_unicode = u('').join + +# function to join list of byte strings +join_bytes = b''.join + +if PY3: + def uascii_to_str(s): + assert isinstance(s, unicode) + return s + + def bascii_to_str(s): + assert isinstance(s, bytes) + return s.decode("ascii") + + def str_to_uascii(s): + assert isinstance(s, str) + return s + + def str_to_bascii(s): + assert isinstance(s, str) + return s.encode("ascii") + + join_byte_values = join_byte_elems = bytes + + def byte_elem_value(elem): + assert isinstance(elem, int) + return elem + + def iter_byte_values(s): + assert isinstance(s, bytes) + return s + + def iter_byte_chars(s): + assert isinstance(s, bytes) + # FIXME: there has to be a better way to do this + return (bytes([c]) for c in s) + +else: + def uascii_to_str(s): + assert isinstance(s, unicode) + return s.encode("ascii") + + def bascii_to_str(s): + assert isinstance(s, bytes) + return s + + def str_to_uascii(s): + assert isinstance(s, str) + return s.decode("ascii") + + def str_to_bascii(s): + assert isinstance(s, str) + return s + + def join_byte_values(values): + return join_bytes(chr(v) for v in values) + + join_byte_elems = join_bytes + + byte_elem_value = ord + + def iter_byte_values(s): + assert isinstance(s, bytes) + return (ord(c) for c in s) + + def iter_byte_chars(s): + assert isinstance(s, bytes) + return s + +add_doc(uascii_to_str, "helper to convert ascii unicode -> native str") +add_doc(bascii_to_str, "helper to convert ascii bytes -> native str") +add_doc(str_to_uascii, "helper to convert ascii native str -> unicode") +add_doc(str_to_bascii, "helper to convert ascii native str -> bytes") + +# join_byte_values -- function to convert list of ordinal integers to byte string. + +# join_byte_elems -- function to convert list of byte elements to byte string; +# i.e. what's returned by ``b('a')[0]``... +# this is b('a') under PY2, but 97 under PY3. + +# byte_elem_value -- function to convert byte element to integer -- a noop under PY3 + +add_doc(iter_byte_values, "iterate over byte string as sequence of ints 0-255") +add_doc(iter_byte_chars, "iterate over byte string as sequence of 1-byte strings") + +#============================================================================= +# numeric +#============================================================================= +if PY3: + int_types = (int,) + num_types = (int, float) +else: + int_types = (int, long) + num_types = (int, long, float) + +#============================================================================= +# iteration helpers +# +# irange - range iterable / view (xrange under py2, range under py3) +# lrange - range list (range under py2, list(range()) under py3) +# +# imap - map to iterator +# lmap - map to list +#============================================================================= +if PY3: + irange = range + ##def lrange(*a,**k): + ## return list(range(*a,**k)) + + def lmap(*a, **k): + return list(map(*a,**k)) + imap = map + + def iteritems(d): + return d.items() + def itervalues(d): + return d.values() + + def nextgetter(obj): + return obj.__next__ + + izip = zip + +else: + irange = xrange + ##lrange = range + + lmap = map + from itertools import imap, izip + + def iteritems(d): + return d.iteritems() + def itervalues(d): + return d.itervalues() + + def nextgetter(obj): + return obj.next + +add_doc(nextgetter, "return function that yields successive values from iterable") + +#============================================================================= +# typing +#============================================================================= +##def is_mapping(obj): +## # non-exhaustive check, enough to distinguish from lists, etc +## return hasattr(obj, "items") + +#============================================================================= +# introspection +#============================================================================= +if PY3: + method_function_attr = "__func__" +else: + method_function_attr = "im_func" + +def get_method_function(func): + """given (potential) method, return underlying function""" + return getattr(func, method_function_attr, func) + +def get_unbound_method_function(func): + """given unbound method, return underlying function""" + return func if PY3 else func.__func__ + +def error_from(exc, # *, + cause=None): + """ + backward compat hack to suppress exception cause in python3.3+ + + one python < 3.3 support is dropped, can replace all uses with "raise exc from None" + """ + exc.__cause__ = cause + exc.__suppress_context__ = True + return exc + +# legacy alias +suppress_cause = error_from + +#============================================================================= +# input/output +#============================================================================= +if PY3: + _lazy_attrs = dict( + BytesIO="io.BytesIO", + UnicodeIO="io.StringIO", + NativeStringIO="io.StringIO", + SafeConfigParser="configparser.ConfigParser", + ) + + print_ = getattr(builtins, "print") + +else: + _lazy_attrs = dict( + BytesIO="cStringIO.StringIO", + UnicodeIO="StringIO.StringIO", + NativeStringIO="cStringIO.StringIO", + SafeConfigParser="ConfigParser.SafeConfigParser", + ) + + def print_(*args, **kwds): + """The new-style print function.""" + # extract kwd args + fp = kwds.pop("file", sys.stdout) + sep = kwds.pop("sep", None) + end = kwds.pop("end", None) + if kwds: + raise TypeError("invalid keyword arguments") + + # short-circuit if no target + if fp is None: + return + + # use unicode or bytes ? + want_unicode = isinstance(sep, unicode) or isinstance(end, unicode) or \ + any(isinstance(arg, unicode) for arg in args) + + # pick default end sequence + if end is None: + end = u("\n") if want_unicode else "\n" + elif not isinstance(end, unicode_or_bytes_types): + raise TypeError("end must be None or a string") + + # pick default separator + if sep is None: + sep = u(" ") if want_unicode else " " + elif not isinstance(sep, unicode_or_bytes_types): + raise TypeError("sep must be None or a string") + + # write to buffer + first = True + write = fp.write + for arg in args: + if first: + first = False + else: + write(sep) + if not isinstance(arg, basestring): + arg = str(arg) + write(arg) + write(end) + +#============================================================================= +# collections +#============================================================================= +if PY26: + _lazy_attrs['OrderedDict'] = 'passlib.utils.compat._ordered_dict.OrderedDict' +else: + _lazy_attrs['OrderedDict'] = 'collections.OrderedDict' + +#============================================================================= +# context managers +#============================================================================= + +try: + # new in py37 + from contextlib import nullcontext +except ImportError: + + class nullcontext(object): + """ + Context manager that does no additional processing. + """ + def __init__(self, enter_result=None): + self.enter_result = enter_result + + def __enter__(self): + return self.enter_result + + def __exit__(self, *exc_info): + pass + +#============================================================================= +# lazy overlay module +#============================================================================= +from types import ModuleType + +def _import_object(source): + """helper to import object from module; accept format `path.to.object`""" + modname, modattr = source.rsplit(".",1) + mod = __import__(modname, fromlist=[modattr], level=0) + return getattr(mod, modattr) + +class _LazyOverlayModule(ModuleType): + """proxy module which overlays original module, + and lazily imports specified attributes. + + this is mainly used to prevent importing of resources + that are only needed by certain password hashes, + yet allow them to be imported from a single location. + + used by :mod:`passlib.utils`, :mod:`passlib.crypto`, + and :mod:`passlib.utils.compat`. + """ + + @classmethod + def replace_module(cls, name, attrmap): + orig = sys.modules[name] + self = cls(name, attrmap, orig) + sys.modules[name] = self + return self + + def __init__(self, name, attrmap, proxy=None): + ModuleType.__init__(self, name) + self.__attrmap = attrmap + self.__proxy = proxy + self.__log = logging.getLogger(name) + + def __getattr__(self, attr): + proxy = self.__proxy + if proxy and hasattr(proxy, attr): + return getattr(proxy, attr) + attrmap = self.__attrmap + if attr in attrmap: + source = attrmap[attr] + if callable(source): + value = source() + else: + value = _import_object(source) + setattr(self, attr, value) + self.__log.debug("loaded lazy attr %r: %r", attr, value) + return value + raise AttributeError("'module' object has no attribute '%s'" % (attr,)) + + def __repr__(self): + proxy = self.__proxy + if proxy: + return repr(proxy) + else: + return ModuleType.__repr__(self) + + def __dir__(self): + attrs = set(dir(self.__class__)) + attrs.update(self.__dict__) + attrs.update(self.__attrmap) + proxy = self.__proxy + if proxy is not None: + attrs.update(dir(proxy)) + return list(attrs) + +# replace this module with overlay that will lazily import attributes. +_LazyOverlayModule.replace_module(__name__, _lazy_attrs) + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/utils/compat/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/utils/compat/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e55ef6676156a9e5db167d0fa0c6d738a7aeee8c GIT binary patch literal 16081 zcmc(GYj7J^c4jxwcoHN?g71gae263@5;R50k|n=INtR?wvL(kBk_ilV`}V#H9O-->zUfxt(`4zD*H$_vo%|_KN<>>L3K)1s!B~N{)Z_! zsfmB(JGUDk078lZwdzb<|I5Xb#3B`S~Ie0liS z0>^#8NnDVVc*zpugS^aNu~e!_*0L_h-3(v({ESzkU>>s1_6IO$2eWRWNhe?4QeL1L{o-L>GTMs2?J*J=w_}wxuxjx2- zKB*dn(mJ;3Lww3pl_T?R!h^?1I)OCHp2t<=8C1>4wcd4pc`;JwmbDUmzsTLwF2 zL9wyAzMwBIu(z(DPinov1-q9xsqH&_um?DLKL(0VDrmT1AKJCb{lNjri<*OSzdW>P z2@cBxi+pfbyj}KP8K_i7opx4dH_C=(g1hA5peXMOjsWwZ)Iigf(Q3;vsk0_Ejy?w@ zA8_rFx&SAnZoo;Y2XIOry3Pmv!M##1Qv0Mnz-g%;a389J<>=$UwDq?Pe`Lp^CzXTrukFSc+>o8X;0Jq8EK+1 zA0v0lvzWOjls^f6?~6URayWQ+<(c3!JSR=F`ZKJ4faFJ&Aq-W6E3E7Ta`>fn2J@+&a#%dT$W==Sy4YD{Ac}^&xmk^FB;rlu0{ zNL2u$FM6j@JnRDafLrI+xe(rrN8&Mkmh?bal0+#HQBz7Zz9gm+VmK})E?vH3D1PzA>~dm7o>f=Et4d-vsU)P?a9oXEj>)s&$E?AC`M})V ztQt+pGs$q|?eLPU&YEMJWel4&m@^x~Bt%o8P$0RcJ3te4t9a4TUVK#&%Ng!LXLsh< zjWZeiJwyiQAJwhrFYPBVuM2p4Qg^(#mXg&|=XB=)Ce)LK$jbys|K#eD5qjYlI1Ne(5H&}G7I=KJCdZ^)QAGm~G{ z9cF#Kbv3L;qR~()L3Q-j%k@d*QISzeZ#9!fCEar+5sil`nA|MII@jB)@1d*V*eYoC zZs*7`c?B6%Geqx36}=tvZxCN;V~;A~_>!y((Uovg7h(imR105VaMP=aUWm)rQ+mre zMUoX+T8KuFh_A+CkwiR2CB7v&6eOo^>ju)|(R=M9D8s=vK{8fWUktzTRe_t#?*$tE?

j~U-Luiv_^^-UGL{-W2fIsI(3I`^1C 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 P^Go0NZ+EW9+nxUd%i8NO literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/utils/compat/_ordered_dict.py b/ansible/lib/python3.11/site-packages/passlib/utils/compat/_ordered_dict.py new file mode 100644 index 000000000..cfd766db3 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/utils/compat/_ordered_dict.py @@ -0,0 +1,242 @@ +"""passlib.utils.compat._ordered_dict -- backport of collections.OrderedDict for py26 + +taken from stdlib-suggested recipe at http://code.activestate.com/recipes/576693/ + +this should be imported from passlib.utils.compat.OrderedDict, not here. +""" + +try: + from thread import get_ident as _get_ident +except ImportError: + from dummy_thread import get_ident as _get_ident + +class OrderedDict(dict): + """Dictionary that remembers insertion order""" + # An inherited dict maps keys to values. + # The inherited dict provides __getitem__, __len__, __contains__, and get. + # The remaining methods are order-aware. + # Big-O running times for all methods are the same as for regular dictionaries. + + # The internal self.__map dictionary maps keys to links in a doubly linked list. + # The circular doubly linked list starts and ends with a sentinel element. + # The sentinel element never gets deleted (this simplifies the algorithm). + # Each link is stored as a list of length three: [PREV, NEXT, KEY]. + + def __init__(self, *args, **kwds): + '''Initialize an ordered dictionary. Signature is the same as for + regular dictionaries, but keyword arguments are not recommended + because their insertion order is arbitrary. + + ''' + if len(args) > 1: + raise TypeError('expected at most 1 arguments, got %d' % len(args)) + try: + self.__root + except AttributeError: + self.__root = root = [] # sentinel node + root[:] = [root, root, None] + self.__map = {} + self.__update(*args, **kwds) + + def __setitem__(self, key, value, dict_setitem=dict.__setitem__): + 'od.__setitem__(i, y) <==> od[i]=y' + # Setting a new item creates a new link which goes at the end of the linked + # list, and the inherited dictionary is updated with the new key/value pair. + if key not in self: + root = self.__root + last = root[0] + last[1] = root[0] = self.__map[key] = [last, root, key] + dict_setitem(self, key, value) + + def __delitem__(self, key, dict_delitem=dict.__delitem__): + 'od.__delitem__(y) <==> del od[y]' + # Deleting an existing item uses self.__map to find the link which is + # then removed by updating the links in the predecessor and successor nodes. + dict_delitem(self, key) + link_prev, link_next, key = self.__map.pop(key) + link_prev[1] = link_next + link_next[0] = link_prev + + def __iter__(self): + 'od.__iter__() <==> iter(od)' + root = self.__root + curr = root[1] + while curr is not root: + yield curr[2] + curr = curr[1] + + def __reversed__(self): + 'od.__reversed__() <==> reversed(od)' + root = self.__root + curr = root[0] + while curr is not root: + yield curr[2] + curr = curr[0] + + def clear(self): + 'od.clear() -> None. Remove all items from od.' + try: + for node in self.__map.itervalues(): + del node[:] + root = self.__root + root[:] = [root, root, None] + self.__map.clear() + except AttributeError: + pass + dict.clear(self) + + def popitem(self, last=True): + '''od.popitem() -> (k, v), return and remove a (key, value) pair. + Pairs are returned in LIFO order if last is true or FIFO order if false. + + ''' + if not self: + raise KeyError('dictionary is empty') + root = self.__root + if last: + link = root[0] + link_prev = link[0] + link_prev[1] = root + root[0] = link_prev + else: + link = root[1] + link_next = link[1] + root[1] = link_next + link_next[0] = root + key = link[2] + del self.__map[key] + value = dict.pop(self, key) + return key, value + + # -- the following methods do not depend on the internal structure -- + + def keys(self): + 'od.keys() -> list of keys in od' + return list(self) + + def values(self): + 'od.values() -> list of values in od' + return [self[key] for key in self] + + def items(self): + 'od.items() -> list of (key, value) pairs in od' + return [(key, self[key]) for key in self] + + def iterkeys(self): + 'od.iterkeys() -> an iterator over the keys in od' + return iter(self) + + def itervalues(self): + 'od.itervalues -> an iterator over the values in od' + for k in self: + yield self[k] + + def iteritems(self): + 'od.iteritems -> an iterator over the (key, value) items in od' + for k in self: + yield (k, self[k]) + + def update(*args, **kwds): + '''od.update(E, **F) -> None. Update od from dict/iterable E and F. + + If E is a dict instance, does: for k in E: od[k] = E[k] + If E has a .keys() method, does: for k in E.keys(): od[k] = E[k] + Or if E is an iterable of items, does: for k, v in E: od[k] = v + In either case, this is followed by: for k, v in F.items(): od[k] = v + + ''' + if len(args) > 2: + raise TypeError('update() takes at most 2 positional ' + 'arguments (%d given)' % (len(args),)) + elif not args: + raise TypeError('update() takes at least 1 argument (0 given)') + self = args[0] + # Make progressively weaker assumptions about "other" + other = () + if len(args) == 2: + other = args[1] + if isinstance(other, dict): + for key in other: + self[key] = other[key] + elif hasattr(other, 'keys'): + for key in other.keys(): + self[key] = other[key] + else: + for key, value in other: + self[key] = value + for key, value in kwds.items(): + self[key] = value + + __update = update # let subclasses override update without breaking __init__ + + __marker = object() + + def pop(self, key, default=__marker): + '''od.pop(k[,d]) -> v, remove specified key and return the corresponding value. + If key is not found, d is returned if given, otherwise KeyError is raised. + + ''' + if key in self: + result = self[key] + del self[key] + return result + if default is self.__marker: + raise KeyError(key) + return default + + def setdefault(self, key, default=None): + 'od.setdefault(k[,d]) -> od.get(k,d), also set od[k]=d if k not in od' + if key in self: + return self[key] + self[key] = default + return default + + def __repr__(self, _repr_running={}): + 'od.__repr__() <==> repr(od)' + call_key = id(self), _get_ident() + if call_key in _repr_running: + return '...' + _repr_running[call_key] = 1 + try: + if not self: + return '%s()' % (self.__class__.__name__,) + return '%s(%r)' % (self.__class__.__name__, self.items()) + finally: + del _repr_running[call_key] + + def __reduce__(self): + 'Return state information for pickling' + items = [[k, self[k]] for k in self] + inst_dict = vars(self).copy() + for k in vars(OrderedDict()): + inst_dict.pop(k, None) + if inst_dict: + return (self.__class__, (items,), inst_dict) + return self.__class__, (items,) + + def copy(self): + 'od.copy() -> a shallow copy of od' + return self.__class__(self) + + @classmethod + def fromkeys(cls, iterable, value=None): + '''OD.fromkeys(S[, v]) -> New ordered dictionary with keys from S + and values equal to v (which defaults to None). + + ''' + d = cls() + for key in iterable: + d[key] = value + return d + + def __eq__(self, other): + '''od.__eq__(y) <==> od==y. Comparison to another OD is order-sensitive + while comparison to a regular mapping is order-insensitive. + + ''' + if isinstance(other, OrderedDict): + return len(self)==len(other) and self.items() == other.items() + return dict.__eq__(self, other) + + def __ne__(self, other): + return not self == other diff --git a/ansible/lib/python3.11/site-packages/passlib/utils/decor.py b/ansible/lib/python3.11/site-packages/passlib/utils/decor.py new file mode 100644 index 000000000..9041d5d04 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/utils/decor.py @@ -0,0 +1,233 @@ +""" +passlib.utils.decor -- helper decorators & properties +""" +#============================================================================= +# imports +#============================================================================= +# core +from __future__ import absolute_import, division, print_function +import logging +log = logging.getLogger(__name__) +from functools import wraps, update_wrapper +import types +from warnings import warn +# site +# pkg +from passlib.utils.compat import PY3 +# local +__all__ = [ + "classproperty", + "hybrid_method", + + "memoize_single_value", + "memoized_property", + + "deprecated_function", + "deprecated_method", +] + +#============================================================================= +# class-level decorators +#============================================================================= +class classproperty(object): + """Function decorator which acts like a combination of classmethod+property (limited to read-only properties)""" + + def __init__(self, func): + self.im_func = func + + def __get__(self, obj, cls): + return self.im_func(cls) + + @property + def __func__(self): + """py3 compatible alias""" + return self.im_func + +class hybrid_method(object): + """ + decorator which invokes function with class if called as class method, + and with object if called at instance level. + """ + + def __init__(self, func): + self.func = func + update_wrapper(self, func) + + def __get__(self, obj, cls): + if obj is None: + obj = cls + if PY3: + return types.MethodType(self.func, obj) + else: + return types.MethodType(self.func, obj, cls) + +#============================================================================= +# memoization +#============================================================================= + +def memoize_single_value(func): + """ + decorator for function which takes no args, + and memoizes result. exposes a ``.clear_cache`` method + to clear the cached value. + """ + cache = {} + + @wraps(func) + def wrapper(): + try: + return cache[True] + except KeyError: + pass + value = cache[True] = func() + return value + + def clear_cache(): + cache.pop(True, None) + wrapper.clear_cache = clear_cache + + return wrapper + +class memoized_property(object): + """ + decorator which invokes method once, then replaces attr with result + """ + def __init__(self, func): + self.__func__ = func + self.__name__ = func.__name__ + self.__doc__ = func.__doc__ + + def __get__(self, obj, cls): + if obj is None: + return self + value = self.__func__(obj) + setattr(obj, self.__name__, value) + return value + + if not PY3: + + @property + def im_func(self): + """py2 alias""" + return self.__func__ + + def clear_cache(self, obj): + """ + class-level helper to clear stored value (if any). + + usage: :samp:`type(self).{attr}.clear_cache(self)` + """ + obj.__dict__.pop(self.__name__, None) + + def peek_cache(self, obj, default=None): + """ + class-level helper to peek at stored value + + usage: :samp:`value = type(self).{attr}.clear_cache(self)` + """ + return obj.__dict__.get(self.__name__, default) + +# works but not used +##class memoized_class_property(object): +## """function decorator which calls function as classmethod, +## and replaces itself with result for current and all future invocations. +## """ +## def __init__(self, func): +## self.im_func = func +## +## def __get__(self, obj, cls): +## func = self.im_func +## value = func(cls) +## setattr(cls, func.__name__, value) +## return value +## +## @property +## def __func__(self): +## "py3 compatible alias" + +#============================================================================= +# deprecation +#============================================================================= +def deprecated_function(msg=None, deprecated=None, removed=None, updoc=True, + replacement=None, _is_method=False, + func_module=None): + """decorator to deprecate a function. + + :arg msg: optional msg, default chosen if omitted + :kwd deprecated: version when function was first deprecated + :kwd removed: version when function will be removed + :kwd replacement: alternate name / instructions for replacing this function. + :kwd updoc: add notice to docstring (default ``True``) + """ + if msg is None: + if _is_method: + msg = "the method %(mod)s.%(klass)s.%(name)s() is deprecated" + else: + msg = "the function %(mod)s.%(name)s() is deprecated" + if deprecated: + msg += " as of Passlib %(deprecated)s" + if removed: + msg += ", and will be removed in Passlib %(removed)s" + if replacement: + msg += ", use %s instead" % replacement + msg += "." + def build(func): + is_classmethod = _is_method and isinstance(func, classmethod) + if is_classmethod: + # NOTE: PY26 doesn't support "classmethod().__func__" directly... + func = func.__get__(None, type).__func__ + opts = dict( + mod=func_module or func.__module__, + name=func.__name__, + deprecated=deprecated, + removed=removed, + ) + if _is_method: + def wrapper(*args, **kwds): + tmp = opts.copy() + klass = args[0] if is_classmethod else args[0].__class__ + tmp.update(klass=klass.__name__, mod=klass.__module__) + warn(msg % tmp, DeprecationWarning, stacklevel=2) + return func(*args, **kwds) + else: + text = msg % opts + def wrapper(*args, **kwds): + warn(text, DeprecationWarning, stacklevel=2) + return func(*args, **kwds) + update_wrapper(wrapper, func) + if updoc and (deprecated or removed) and \ + wrapper.__doc__ and ".. deprecated::" not in wrapper.__doc__: + txt = deprecated or '' + if removed or replacement: + txt += "\n " + if removed: + txt += "and will be removed in version %s" % (removed,) + if replacement: + if removed: + txt += ", " + txt += "use %s instead" % replacement + txt += "." + if not wrapper.__doc__.strip(" ").endswith("\n"): + wrapper.__doc__ += "\n" + wrapper.__doc__ += "\n.. deprecated:: %s\n" % (txt,) + if is_classmethod: + wrapper = classmethod(wrapper) + return wrapper + return build + +def deprecated_method(msg=None, deprecated=None, removed=None, updoc=True, + replacement=None): + """decorator to deprecate a method. + + :arg msg: optional msg, default chosen if omitted + :kwd deprecated: version when method was first deprecated + :kwd removed: version when method will be removed + :kwd replacement: alternate name / instructions for replacing this method. + :kwd updoc: add notice to docstring (default ``True``) + """ + return deprecated_function(msg, deprecated, removed, updoc, replacement, + _is_method=True) + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/utils/des.py b/ansible/lib/python3.11/site-packages/passlib/utils/des.py new file mode 100644 index 000000000..034bfc4d1 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/utils/des.py @@ -0,0 +1,46 @@ +""" +passlib.utils.des - DEPRECATED LOCATION, WILL BE REMOVED IN 2.0 + +This has been moved to :mod:`passlib.crypto.des`. +""" +#============================================================================= +# import from new location +#============================================================================= +from warnings import warn +warn("the 'passlib.utils.des' module has been relocated to 'passlib.crypto.des' " + "as of passlib 1.7, and the old location will be removed in passlib 2.0", + DeprecationWarning) + +#============================================================================= +# relocated functions +#============================================================================= +from passlib.utils.decor import deprecated_function +from passlib.crypto.des import expand_des_key, des_encrypt_block, des_encrypt_int_block + +expand_des_key = deprecated_function(deprecated="1.7", removed="1.8", + replacement="passlib.crypto.des.expand_des_key")(expand_des_key) + +des_encrypt_block = deprecated_function(deprecated="1.7", removed="1.8", + replacement="passlib.crypto.des.des_encrypt_block")(des_encrypt_block) + +des_encrypt_int_block = deprecated_function(deprecated="1.7", removed="1.8", + replacement="passlib.crypto.des.des_encrypt_int_block")(des_encrypt_int_block) + +#============================================================================= +# deprecated functions -- not carried over to passlib.crypto.des +#============================================================================= +import struct +_unpack_uint64 = struct.Struct(">Q").unpack + +@deprecated_function(deprecated="1.6", removed="1.8", + replacement="passlib.crypto.des.des_encrypt_int_block()") +def mdes_encrypt_int_block(key, input, salt=0, rounds=1): # pragma: no cover -- deprecated & unused + if isinstance(key, bytes): + if len(key) == 7: + key = expand_des_key(key) + key = _unpack_uint64(key)[0] + return des_encrypt_int_block(key, input, salt, rounds) + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/utils/handlers.py b/ansible/lib/python3.11/site-packages/passlib/utils/handlers.py new file mode 100644 index 000000000..f8681fa40 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/utils/handlers.py @@ -0,0 +1,2711 @@ +"""passlib.handler - code for implementing handlers, and global registry for handlers""" +#============================================================================= +# imports +#============================================================================= +from __future__ import with_statement +# core +import inspect +import logging; log = logging.getLogger(__name__) +import math +import threading +from warnings import warn +# site +# pkg +import passlib.exc as exc, passlib.ifc as ifc +from passlib.exc import MissingBackendError, PasslibConfigWarning, \ + PasslibHashWarning +from passlib.ifc import PasswordHash +from passlib.registry import get_crypt_handler +from passlib.utils import ( + consteq, getrandstr, getrandbytes, + rng, to_native_str, + is_crypt_handler, to_unicode, + MAX_PASSWORD_SIZE, accepts_keyword, as_bool, + update_mixin_classes) +from passlib.utils.binary import ( + BASE64_CHARS, HASH64_CHARS, PADDED_BASE64_CHARS, + HEX_CHARS, UPPER_HEX_CHARS, LOWER_HEX_CHARS, + ALL_BYTE_VALUES, +) +from passlib.utils.compat import join_byte_values, irange, u, native_string_types, \ + uascii_to_str, join_unicode, unicode, str_to_uascii, \ + join_unicode, unicode_or_bytes_types, PY2, int_types +from passlib.utils.decor import classproperty, deprecated_method +# local +__all__ = [ + # helpers for implementing MCF handlers + 'parse_mc2', + 'parse_mc3', + 'render_mc2', + 'render_mc3', + + # framework for implementing handlers + 'GenericHandler', + 'StaticHandler', + 'HasUserContext', + 'HasRawChecksum', + 'HasManyIdents', + 'HasSalt', + 'HasRawSalt', + 'HasRounds', + 'HasManyBackends', + + # other helpers + 'PrefixWrapper', + + # TODO: a bunch of other things are commonly assumed in this namespace + # (e.g. HEX_CHARS etc); need to audit uses and update this list. +] + +#============================================================================= +# constants +#============================================================================= + +# deprecated aliases - will be removed after passlib 1.8 +H64_CHARS = HASH64_CHARS +B64_CHARS = BASE64_CHARS +PADDED_B64_CHARS = PADDED_BASE64_CHARS +UC_HEX_CHARS = UPPER_HEX_CHARS +LC_HEX_CHARS = LOWER_HEX_CHARS + +#============================================================================= +# support functions +#============================================================================= +def _bitsize(count, chars): + """helper for bitsize() methods""" + if chars and count: + import math + return int(count * math.log(len(chars), 2)) + else: + return 0 + +def guess_app_stacklevel(start=1): + """ + try to guess stacklevel for application warning. + looks for first frame not part of passlib. + """ + frame = inspect.currentframe() + count = -start + try: + while frame: + name = frame.f_globals.get('__name__', "") + if name.startswith("passlib.tests.") or not name.startswith("passlib."): + return max(1, count) + count += 1 + frame = frame.f_back + return start + finally: + del frame + +def warn_hash_settings_deprecation(handler, kwds): + warn("passing settings to %(handler)s.hash() is deprecated, and won't be supported in Passlib 2.0; " + "use '%(handler)s.using(**settings).hash(secret)' instead" % dict(handler=handler.name), + DeprecationWarning, stacklevel=guess_app_stacklevel(2)) + +def extract_settings_kwds(handler, kwds): + """ + helper to extract settings kwds from mix of context & settings kwds. + pops settings keys from kwds, returns them as a dict. + """ + context_keys = set(handler.context_kwds) + return dict((key, kwds.pop(key)) for key in list(kwds) if key not in context_keys) + +#============================================================================= +# parsing helpers +#============================================================================= +_UDOLLAR = u("$") +_UZERO = u("0") + +def validate_secret(secret): + """ensure secret has correct type & size""" + if not isinstance(secret, unicode_or_bytes_types): + raise exc.ExpectedStringError(secret, "secret") + if len(secret) > MAX_PASSWORD_SIZE: + raise exc.PasswordSizeError(MAX_PASSWORD_SIZE) + +def to_unicode_for_identify(hash): + """convert hash to unicode for identify method""" + if isinstance(hash, unicode): + return hash + elif isinstance(hash, bytes): + # try as utf-8, but if it fails, use foolproof latin-1, + # since we don't really care about non-ascii chars + # when running identify. + try: + return hash.decode("utf-8") + except UnicodeDecodeError: + return hash.decode("latin-1") + else: + raise exc.ExpectedStringError(hash, "hash") + +def parse_mc2(hash, prefix, sep=_UDOLLAR, handler=None): + """parse hash using 2-part modular crypt format. + + this expects a hash of the format :samp:`{prefix}{salt}[${checksum}]`, + such as md5_crypt, and parses it into salt / checksum portions. + + :arg hash: the hash to parse (bytes or unicode) + :arg prefix: the identifying prefix (unicode) + :param sep: field separator (unicode, defaults to ``$``). + :param handler: handler class to pass to error constructors. + + :returns: + a ``(salt, chk | None)`` tuple. + """ + # detect prefix + hash = to_unicode(hash, "ascii", "hash") + assert isinstance(prefix, unicode) + if not hash.startswith(prefix): + raise exc.InvalidHashError(handler) + + # parse 2-part hash or 1-part config string + assert isinstance(sep, unicode) + parts = hash[len(prefix):].split(sep) + if len(parts) == 2: + salt, chk = parts + return salt, chk or None + elif len(parts) == 1: + return parts[0], None + else: + raise exc.MalformedHashError(handler) + +def parse_mc3(hash, prefix, sep=_UDOLLAR, rounds_base=10, + default_rounds=None, handler=None): + """parse hash using 3-part modular crypt format. + + this expects a hash of the format :samp:`{prefix}[{rounds}]${salt}[${checksum}]`, + such as sha1_crypt, and parses it into rounds / salt / checksum portions. + tries to convert the rounds to an integer, + and throws error if it has zero-padding. + + :arg hash: the hash to parse (bytes or unicode) + :arg prefix: the identifying prefix (unicode) + :param sep: field separator (unicode, defaults to ``$``). + :param rounds_base: + the numeric base the rounds are encoded in (defaults to base 10). + :param default_rounds: + the default rounds value to return if the rounds field was omitted. + if this is ``None`` (the default), the rounds field is *required*. + :param handler: handler class to pass to error constructors. + + :returns: + a ``(rounds : int, salt, chk | None)`` tuple. + """ + # detect prefix + hash = to_unicode(hash, "ascii", "hash") + assert isinstance(prefix, unicode) + if not hash.startswith(prefix): + raise exc.InvalidHashError(handler) + + # parse 3-part hash or 2-part config string + assert isinstance(sep, unicode) + parts = hash[len(prefix):].split(sep) + if len(parts) == 3: + rounds, salt, chk = parts + elif len(parts) == 2: + rounds, salt = parts + chk = None + else: + raise exc.MalformedHashError(handler) + + # validate & parse rounds portion + if rounds.startswith(_UZERO) and rounds != _UZERO: + raise exc.ZeroPaddedRoundsError(handler) + elif rounds: + rounds = int(rounds, rounds_base) + elif default_rounds is None: + raise exc.MalformedHashError(handler, "empty rounds field") + else: + rounds = default_rounds + + # return result + return rounds, salt, chk or None + +# def parse_mc3_long(hash, prefix, sep=_UDOLLAR, handler=None): +# """ +# parse hash using 3-part modular crypt format, +# with complex settings string instead of simple rounds. +# otherwise works same as :func:`parse_mc3` +# """ +# # detect prefix +# hash = to_unicode(hash, "ascii", "hash") +# assert isinstance(prefix, unicode) +# if not hash.startswith(prefix): +# raise exc.InvalidHashError(handler) +# +# # parse 3-part hash or 2-part config string +# assert isinstance(sep, unicode) +# parts = hash[len(prefix):].split(sep) +# if len(parts) == 3: +# return parts +# elif len(parts) == 2: +# settings, salt = parts +# return settings, salt, None +# else: +# raise exc.MalformedHashError(handler) + +def parse_int(source, base=10, default=None, param="value", handler=None): + """ + helper to parse an integer config field + + :arg source: unicode source string + :param base: numeric base + :param default: optional default if source is empty + :param param: name of variable, for error msgs + :param handler: handler class, for error msgs + """ + if source.startswith(_UZERO) and source != _UZERO: + raise exc.MalformedHashError(handler, "zero-padded %s field" % param) + elif source: + return int(source, base) + elif default is None: + raise exc.MalformedHashError(handler, "empty %s field" % param) + else: + return default + +#============================================================================= +# formatting helpers +#============================================================================= +def render_mc2(ident, salt, checksum, sep=u("$")): + """format hash using 2-part modular crypt format; inverse of parse_mc2() + + returns native string with format :samp:`{ident}{salt}[${checksum}]`, + such as used by md5_crypt. + + :arg ident: identifier prefix (unicode) + :arg salt: encoded salt (unicode) + :arg checksum: encoded checksum (unicode or None) + :param sep: separator char (unicode, defaults to ``$``) + + :returns: + config or hash (native str) + """ + if checksum: + parts = [ident, salt, sep, checksum] + else: + parts = [ident, salt] + return uascii_to_str(join_unicode(parts)) + +def render_mc3(ident, rounds, salt, checksum, sep=u("$"), rounds_base=10): + """format hash using 3-part modular crypt format; inverse of parse_mc3() + + returns native string with format :samp:`{ident}[{rounds}$]{salt}[${checksum}]`, + such as used by sha1_crypt. + + :arg ident: identifier prefix (unicode) + :arg rounds: rounds field (int or None) + :arg salt: encoded salt (unicode) + :arg checksum: encoded checksum (unicode or None) + :param sep: separator char (unicode, defaults to ``$``) + :param rounds_base: base to encode rounds value (defaults to base 10) + + :returns: + config or hash (native str) + """ + if rounds is None: + rounds = u('') + elif rounds_base == 16: + rounds = u("%x") % rounds + else: + assert rounds_base == 10 + rounds = unicode(rounds) + if checksum: + parts = [ident, rounds, sep, salt, sep, checksum] + else: + parts = [ident, rounds, sep, salt] + return uascii_to_str(join_unicode(parts)) + + +def mask_value(value, show=4, pct=0.125, char=u"*"): + """ + helper to mask contents of sensitive field. + + :param value: + raw value (str, bytes, etc) + + :param show: + max # of characters to remain visible + + :param pct: + don't show more than this % of input. + + :param char: + character to use for masking + + :rtype: str | None + """ + if value is None: + return None + if not isinstance(value, unicode): + if isinstance(value, bytes): + from passlib.utils.binary import ab64_encode + value = ab64_encode(value).decode("ascii") + else: + value = unicode(value) + size = len(value) + show = min(show, int(size * pct)) + return value[:show] + char * (size - show) + +#============================================================================= +# parameter helpers +#============================================================================= + +def validate_default_value(handler, default, norm, param="value"): + """ + assert helper that quickly validates default value. + designed to get out of the way and reduce overhead when asserts are stripped. + """ + assert default is not None, "%s lacks default %s" % (handler.name, param) + assert norm(default) == default, "%s: invalid default %s: %r" % (handler.name, param, default) + return True + +def norm_integer(handler, value, min=1, max=None, # * + param="value", relaxed=False): + """ + helper to normalize and validate an integer value (e.g. rounds, salt_size) + + :arg value: value provided to constructor + :arg default: default value if none provided. if set to ``None``, value is required. + :arg param: name of parameter (xxx: move to first arg?) + :param min: minimum value (defaults to 1) + :param max: maximum value (default ``None`` means no maximum) + :returns: validated value + """ + # check type + if not isinstance(value, int_types): + raise exc.ExpectedTypeError(value, "integer", param) + + # check minimum + if value < min: + msg = "%s: %s (%d) is too low, must be at least %d" % (handler.name, param, value, min) + if relaxed: + warn(msg, exc.PasslibHashWarning) + value = min + else: + raise ValueError(msg) + + # check maximum + if max and value > max: + msg = "%s: %s (%d) is too large, cannot be more than %d" % (handler.name, param, value, max) + if relaxed: + warn(msg, exc.PasslibHashWarning) + value = max + else: + raise ValueError(msg) + + return value + +#============================================================================= +# MinimalHandler +#============================================================================= +class MinimalHandler(PasswordHash): + """ + helper class for implementing hash handlers. + provides nothing besides a base implementation of the .using() subclass constructor. + """ + #=================================================================== + # class attr + #=================================================================== + + #: private flag used by using() constructor to detect if this is already a subclass. + _configured = False + + #=================================================================== + # configuration interface + #=================================================================== + + @classmethod + def using(cls, relaxed=False): + # NOTE: this provides the base implementation, which takes care of + # creating the newly configured class. Mixins and subclasses + # should wrap this, and modify the returned class to suit their options. + # NOTE: 'relaxed' keyword is ignored here, but parsed so that subclasses + # can check for it as argument, and modify their parsing behavior accordingly. + name = cls.__name__ + if not cls._configured: + # TODO: straighten out class naming, repr, and .name attr + name = "" % name + return type(name, (cls,), dict(__module__=cls.__module__, _configured=True)) + + #=================================================================== + # eoc + #=================================================================== + +class TruncateMixin(MinimalHandler): + """ + PasswordHash mixin which provides a method + that will check if secret would be truncated, + and can be configured to throw an error. + + .. warning:: + + Hashers using this mixin will generally need to override + the default PasswordHash.truncate_error policy of "True", + and will similarly want to override .truncate_verify_reject as well. + + TODO: This should be done explicitly, but for now this mixin sets + these flags implicitly. + """ + + truncate_error = False + truncate_verify_reject = False + + @classmethod + def using(cls, truncate_error=None, **kwds): + subcls = super(TruncateMixin, cls).using(**kwds) + if truncate_error is not None: + truncate_error = as_bool(truncate_error, param="truncate_error") + if truncate_error is not None: + subcls.truncate_error = truncate_error + return subcls + + @classmethod + def _check_truncate_policy(cls, secret): + """ + make sure secret won't be truncated. + NOTE: this should only be called for .hash(), not for .verify(), + which should honor the .truncate_verify_reject policy. + """ + assert cls.truncate_size is not None, "truncate_size must be set by subclass" + if cls.truncate_error and len(secret) > cls.truncate_size: + raise exc.PasswordTruncateError(cls) + +#============================================================================= +# GenericHandler +#============================================================================= +class GenericHandler(MinimalHandler): + """helper class for implementing hash handlers. + + GenericHandler-derived classes will have (at least) the following + constructor options, though others may be added by mixins + and by the class itself: + + :param checksum: + this should contain the digest portion of a + parsed hash (mainly provided when the constructor is called + by :meth:`from_string()`). + defaults to ``None``. + + :param use_defaults: + If ``False`` (the default), a :exc:`TypeError` should be thrown + if any settings required by the handler were not explicitly provided. + + If ``True``, the handler should attempt to provide a default for any + missing values. This means generate missing salts, fill in default + cost parameters, etc. + + This is typically only set to ``True`` when the constructor + is called by :meth:`hash`, allowing user-provided values + to be handled in a more permissive manner. + + :param relaxed: + If ``False`` (the default), a :exc:`ValueError` should be thrown + if any settings are out of bounds or otherwise invalid. + + If ``True``, they should be corrected if possible, and a warning + issue. If not possible, only then should an error be raised. + (e.g. under ``relaxed=True``, rounds values will be clamped + to min/max rounds). + + This is mainly used when parsing the config strings of certain + hashes, whose specifications implementations to be tolerant + of incorrect values in salt strings. + + Class Attributes + ================ + + .. attribute:: ident + + [optional] + If this attribute is filled in, the default :meth:`identify` method will use + it as a identifying prefix that can be used to recognize instances of this handler's + hash. Filling this out is recommended for speed. + + This should be a unicode str. + + .. attribute:: _hash_regex + + [optional] + If this attribute is filled in, the default :meth:`identify` method + will use it to recognize instances of the hash. If :attr:`ident` + is specified, this will be ignored. + + This should be a unique regex object. + + .. attribute:: checksum_size + + [optional] + Specifies the number of characters that should be expected in the checksum string. + If omitted, no check will be performed. + + .. attribute:: checksum_chars + + [optional] + A string listing all the characters allowed in the checksum string. + If omitted, no check will be performed. + + This should be a unicode str. + + .. attribute:: _stub_checksum + + Placeholder checksum that will be used by genconfig() + in lieu of actually generating a hash for the empty string. + This should be a string of the same datatype as :attr:`checksum`. + + Instance Attributes + =================== + .. attribute:: checksum + + The checksum string provided to the constructor (after passing it + through :meth:`_norm_checksum`). + + Required Subclass Methods + ========================= + The following methods must be provided by handler subclass: + + .. automethod:: from_string + .. automethod:: to_string + .. automethod:: _calc_checksum + + Default Methods + =============== + The following methods have default implementations that should work for + most cases, though they may be overridden if the hash subclass needs to: + + .. automethod:: _norm_checksum + + .. automethod:: genconfig + .. automethod:: genhash + .. automethod:: identify + .. automethod:: hash + .. automethod:: verify + """ + + #=================================================================== + # class attr + #=================================================================== + # this must be provided by the actual class. + setting_kwds = None + + # providing default since most classes don't use this at all. + context_kwds = () + + # optional prefix that uniquely identifies hash + ident = None + + # optional regexp for recognizing hashes, + # used by default identify() if .ident isn't specified. + _hash_regex = None + + # if specified, _norm_checksum will require this length + checksum_size = None + + # if specified, _norm_checksum() will validate this + checksum_chars = None + + # private flag used by HasRawChecksum + _checksum_is_bytes = False + + #=================================================================== + # instance attrs + #=================================================================== + checksum = None # stores checksum +# use_defaults = False # whether _norm_xxx() funcs should fill in defaults. +# relaxed = False # when _norm_xxx() funcs should be strict about inputs + + #=================================================================== + # init + #=================================================================== + def __init__(self, checksum=None, use_defaults=False, **kwds): + self.use_defaults = use_defaults + super(GenericHandler, self).__init__(**kwds) + if checksum is not None: + # XXX: do we need to set .relaxed for checksum coercion? + self.checksum = self._norm_checksum(checksum) + + # NOTE: would like to make this classmethod, but fshp checksum size + # is dependant on .variant, so leaving this as instance method. + def _norm_checksum(self, checksum, relaxed=False): + """validates checksum keyword against class requirements, + returns normalized version of checksum. + """ + # NOTE: by default this code assumes checksum should be unicode. + # For classes where the checksum is raw bytes, the HasRawChecksum sets + # the _checksum_is_bytes flag which alters various code paths below. + + # normalize to bytes / unicode + raw = self._checksum_is_bytes + if raw: + # NOTE: no clear route to reasonably convert unicode -> raw bytes, + # so 'relaxed' does nothing here + if not isinstance(checksum, bytes): + raise exc.ExpectedTypeError(checksum, "bytes", "checksum") + + elif not isinstance(checksum, unicode): + if isinstance(checksum, bytes) and relaxed: + warn("checksum should be unicode, not bytes", PasslibHashWarning) + checksum = checksum.decode("ascii") + else: + raise exc.ExpectedTypeError(checksum, "unicode", "checksum") + + # check size + cc = self.checksum_size + if cc and len(checksum) != cc: + raise exc.ChecksumSizeError(self, raw=raw) + + # check charset + if not raw: + cs = self.checksum_chars + if cs and any(c not in cs for c in checksum): + raise ValueError("invalid characters in %s checksum" % (self.name,)) + + return checksum + + #=================================================================== + # password hash api - formatting interface + #=================================================================== + @classmethod + def identify(cls, hash): + # NOTE: subclasses may wish to use faster / simpler identify, + # and raise value errors only when an invalid (but identifiable) + # string is parsed + hash = to_unicode_for_identify(hash) + if not hash: + return False + + # does class specify a known unique prefix to look for? + ident = cls.ident + if ident is not None: + return hash.startswith(ident) + + # does class provide a regexp to use? + pat = cls._hash_regex + if pat is not None: + return pat.match(hash) is not None + + # as fallback, try to parse hash, and see if we succeed. + # inefficient, but works for most cases. + try: + cls.from_string(hash) + return True + except ValueError: + return False + + @classmethod + def from_string(cls, hash, **context): # pragma: no cover + r""" + return parsed instance from hash/configuration string + + :param \\*\\*context: + context keywords to pass to constructor (if applicable). + + :raises ValueError: if hash is incorrectly formatted + + :returns: + hash parsed into components, + for formatting / calculating checksum. + """ + raise NotImplementedError("%s must implement from_string()" % (cls,)) + + def to_string(self): # pragma: no cover + """render instance to hash or configuration string + + :returns: + hash string with salt & digest included. + + should return native string type (ascii-bytes under python 2, + unicode under python 3) + """ + raise NotImplementedError("%s must implement from_string()" % (self.__class__,)) + + #=================================================================== + # checksum generation + #=================================================================== + + # NOTE: this is only used by genconfig(), and will be removed in passlib 2.0 + @property + def _stub_checksum(self): + """ + placeholder used by default .genconfig() so it can avoid expense of calculating digest. + """ + # used fixed string if available + if self.checksum_size: + if self._checksum_is_bytes: + return b'\x00' * self.checksum_size + if self.checksum_chars: + return self.checksum_chars[0] * self.checksum_size + + # hack to minimize cost of calculating real checksum + if isinstance(self, HasRounds): + orig = self.rounds + self.rounds = self.min_rounds or 1 + try: + return self._calc_checksum("") + finally: + self.rounds = orig + + # final fallback, generate a real checksum + return self._calc_checksum("") + + def _calc_checksum(self, secret): # pragma: no cover + """given secret; calcuate and return encoded checksum portion of hash + string, taking config from object state + + calc checksum implementations may assume secret is always + either unicode or bytes, checks are performed by verify/etc. + """ + raise NotImplementedError("%s must implement _calc_checksum()" % + (self.__class__,)) + + #=================================================================== + #'application' interface (default implementation) + #=================================================================== + + @classmethod + def hash(cls, secret, **kwds): + if kwds: + # Deprecating passing any settings keywords via .hash() as of passlib 1.7; everything + # should use .using().hash() instead. If any keywords are specified, presume they're + # context keywords by default (the common case), and extract out any settings kwds. + # Support for passing settings via .hash() will be removed in Passlib 2.0, along with + # this block of code. + settings = extract_settings_kwds(cls, kwds) + if settings: + warn_hash_settings_deprecation(cls, settings) + return cls.using(**settings).hash(secret, **kwds) + # NOTE: at this point, 'kwds' should just contain context_kwds subset + validate_secret(secret) + self = cls(use_defaults=True, **kwds) + self.checksum = self._calc_checksum(secret) + return self.to_string() + + @classmethod + def verify(cls, secret, hash, **context): + # NOTE: classes with multiple checksum encodings should either + # override this method, or ensure that from_string() / _norm_checksum() + # ensures .checksum always uses a single canonical representation. + validate_secret(secret) + self = cls.from_string(hash, **context) + chk = self.checksum + if chk is None: + raise exc.MissingDigestError(cls) + return consteq(self._calc_checksum(secret), chk) + + #=================================================================== + # legacy crypt interface + #=================================================================== + + @deprecated_method(deprecated="1.7", removed="2.0") + @classmethod + def genconfig(cls, **kwds): + # NOTE: 'kwds' should generally always be settings, so after this completes, *should* be empty. + settings = extract_settings_kwds(cls, kwds) + if settings: + return cls.using(**settings).genconfig(**kwds) + # NOTE: this uses optional stub checksum to bypass potentially expensive digest generation, + # when caller just wants the config string. + self = cls(use_defaults=True, **kwds) + self.checksum = self._stub_checksum + return self.to_string() + + @deprecated_method(deprecated="1.7", removed="2.0") + @classmethod + def genhash(cls, secret, config, **context): + if config is None: + raise TypeError("config must be string") + validate_secret(secret) + self = cls.from_string(config, **context) + self.checksum = self._calc_checksum(secret) + return self.to_string() + + #=================================================================== + # migration interface (basde implementation) + #=================================================================== + + @classmethod + def needs_update(cls, hash, secret=None, **kwds): + # NOTE: subclasses should generally just wrap _calc_needs_update() + # to check their particular keywords. + self = cls.from_string(hash) + assert isinstance(self, cls) + return self._calc_needs_update(secret=secret, **kwds) + + def _calc_needs_update(self, secret=None): + """ + internal helper for :meth:`needs_update`. + """ + # NOTE: this just provides a stub, subclasses & mixins + # should override this with their own tests. + return False + + #=================================================================== + # experimental - the following methods are not finished or tested, + # but way work correctly for some hashes + #=================================================================== + + #: internal helper for forcing settings to be included, even if default matches + _always_parse_settings = () + + #: internal helper for excluding certain setting_kwds from parsehash() result + _unparsed_settings = ("salt_size", "relaxed") + + #: parsehash() keys that need to be sanitized + _unsafe_settings = ("salt", "checksum") + + @classproperty + def _parsed_settings(cls): + """ + helper for :meth:`parsehash` -- + returns list of attributes which should be extracted by parse_hash() from hasher object. + + default implementation just takes setting_kwds, and excludes _unparsed_settings + """ + return tuple(key for key in cls.setting_kwds if key not in cls._unparsed_settings) + + @classmethod + def parsehash(cls, hash, checksum=True, sanitize=False): + """[experimental method] parse hash into dictionary of settings. + + this essentially acts as the inverse of :meth:`hash`: for most + cases, if ``hash = cls.hash(secret, **opts)``, then + ``cls.parsehash(hash)`` will return a dict matching the original options + (with the extra keyword *checksum*). + + this method may not work correctly for all hashes, + and may not be available on some few. its interface may + change in future releases, if it's kept around at all. + + :arg hash: hash to parse + :param checksum: include checksum keyword? (defaults to True) + :param sanitize: mask data for sensitive fields? (defaults to False) + """ + # FIXME: this may not work for hashes with non-standard settings. + # XXX: how should this handle checksum/salt encoding? + # need to work that out for hash() anyways. + self = cls.from_string(hash) + # XXX: could split next few lines out as self._parsehash() for subclassing + # XXX: could try to resolve ident/variant to publically suitable alias. + # XXX: for v1.8, consider making "always" the default policy, and compare to class default + # only for whitelisted attrs? or make this whole method obsolete by reworking + # so "hasher" object & it's attrs are public? + UNSET = object() + always = self._always_parse_settings + kwds = dict((key, getattr(self, key)) for key in self._parsed_settings + if key in always or getattr(self, key) != getattr(cls, key, UNSET)) + if checksum and self.checksum is not None: + kwds['checksum'] = self.checksum + if sanitize: + if sanitize is True: + sanitize = mask_value + for key in cls._unsafe_settings: + if key in kwds: + kwds[key] = sanitize(kwds[key]) + return kwds + + @classmethod + def bitsize(cls, **kwds): + """[experimental method] return info about bitsizes of hash""" + try: + info = super(GenericHandler, cls).bitsize(**kwds) + except AttributeError: + info = {} + cc = ALL_BYTE_VALUES if cls._checksum_is_bytes else cls.checksum_chars + if cls.checksum_size and cc: + # FIXME: this may overestimate size due to padding bits (e.g. bcrypt) + # FIXME: this will be off by 1 for case-insensitive hashes. + info['checksum'] = _bitsize(cls.checksum_size, cc) + return info + + #=================================================================== + # eoc + #=================================================================== + +class StaticHandler(GenericHandler): + """GenericHandler mixin for classes which have no settings. + + This mixin assumes the entirety of the hash ise stored in the + :attr:`checksum` attribute; that the hash has no rounds, salt, + etc. This class provides the following: + + * a default :meth:`genconfig` that always returns None. + * a default :meth:`from_string` and :meth:`to_string` + that store the entire hash within :attr:`checksum`, + after optionally stripping a constant prefix. + + All that is required by subclasses is an implementation of + the :meth:`_calc_checksum` method. + """ + # TODO: document _norm_hash() + + setting_kwds = () + + # optional constant prefix subclasses can specify + _hash_prefix = u("") + + @classmethod + def from_string(cls, hash, **context): + # default from_string() which strips optional prefix, + # and passes rest unchanged as checksum value. + hash = to_unicode(hash, "ascii", "hash") + hash = cls._norm_hash(hash) + # could enable this for extra strictness + ##pat = cls._hash_regex + ##if pat and pat.match(hash) is None: + ## raise ValueError("not a valid %s hash" % (cls.name,)) + prefix = cls._hash_prefix + if prefix: + if hash.startswith(prefix): + hash = hash[len(prefix):] + else: + raise exc.InvalidHashError(cls) + return cls(checksum=hash, **context) + + @classmethod + def _norm_hash(cls, hash): + """helper for subclasses to normalize case if needed""" + return hash + + def to_string(self): + return uascii_to_str(self._hash_prefix + self.checksum) + + # per-subclass: stores dynamically created subclass used by _calc_checksum() stub + __cc_compat_hack = None + + def _calc_checksum(self, secret): + """given secret; calcuate and return encoded checksum portion of hash + string, taking config from object state + """ + # NOTE: prior to 1.6, StaticHandler required classes implement genhash + # instead of this method. so if we reach here, we try calling genhash. + # if that succeeds, we issue deprecation warning. if it fails, + # we'll just recurse back to here, but in a different instance. + # so before we call genhash, we create a subclass which handles + # throwing the NotImplementedError. + cls = self.__class__ + assert cls.__module__ != __name__ + wrapper_cls = cls.__cc_compat_hack + if wrapper_cls is None: + def inner(self, secret): + raise NotImplementedError("%s must implement _calc_checksum()" % + (cls,)) + wrapper_cls = cls.__cc_compat_hack = type(cls.__name__ + "_wrapper", + (cls,), dict(_calc_checksum=inner, __module__=cls.__module__)) + context = dict((k,getattr(self,k)) for k in self.context_kwds) + # NOTE: passing 'config=None' here even though not currently allowed by ifc, + # since it *is* allowed under the old 1.5 ifc we're checking for here. + try: + hash = wrapper_cls.genhash(secret, None, **context) + except TypeError as err: + if str(err) == "config must be string": + raise NotImplementedError("%s must implement _calc_checksum()" % + (cls,)) + else: + raise + warn("%r should be updated to implement StaticHandler._calc_checksum() " + "instead of StaticHandler.genhash(), support for the latter " + "style will be removed in Passlib 1.8" % cls, + DeprecationWarning) + return str_to_uascii(hash) + +#============================================================================= +# GenericHandler mixin classes +#============================================================================= +class HasEncodingContext(GenericHandler): + """helper for classes which require knowledge of the encoding used""" + context_kwds = ("encoding",) + default_encoding = "utf-8" + + def __init__(self, encoding=None, **kwds): + super(HasEncodingContext, self).__init__(**kwds) + self.encoding = encoding or self.default_encoding + +class HasUserContext(GenericHandler): + """helper for classes which require a user context keyword""" + context_kwds = ("user",) + + def __init__(self, user=None, **kwds): + super(HasUserContext, self).__init__(**kwds) + self.user = user + + # XXX: would like to validate user input here, but calls to from_string() + # which lack context keywords would then fail; so leaving code per-handler. + + # wrap funcs to accept 'user' as positional arg for ease of use. + @classmethod + def hash(cls, secret, user=None, **context): + return super(HasUserContext, cls).hash(secret, user=user, **context) + + @classmethod + def verify(cls, secret, hash, user=None, **context): + return super(HasUserContext, cls).verify(secret, hash, user=user, **context) + + @deprecated_method(deprecated="1.7", removed="2.0") + @classmethod + def genhash(cls, secret, config, user=None, **context): + return super(HasUserContext, cls).genhash(secret, config, user=user, **context) + + # XXX: how to guess the entropy of a username? + # most of these hashes are for a system (e.g. Oracle) + # which has a few *very common* names and thus really low entropy; + # while the rest are slightly less predictable. + # need to find good reference about this. + ##@classmethod + ##def bitsize(cls, **kwds): + ## info = super(HasUserContext, cls).bitsize(**kwds) + ## info['user'] = xxx + ## return info + +#------------------------------------------------------------------------ +# checksum mixins +#------------------------------------------------------------------------ +class HasRawChecksum(GenericHandler): + """mixin for classes which work with decoded checksum bytes + + .. todo:: + + document this class's usage + """ + # NOTE: GenericHandler.checksum_chars is ignored by this implementation. + + # NOTE: all HasRawChecksum code is currently part of GenericHandler, + # using private '_checksum_is_bytes' flag. + # this arrangement may be changed in the future. + _checksum_is_bytes = True + +#------------------------------------------------------------------------ +# ident mixins +#------------------------------------------------------------------------ +class HasManyIdents(GenericHandler): + """mixin for hashes which use multiple prefix identifiers + + For the hashes which may use multiple identifier prefixes, + this mixin adds an ``ident`` keyword to constructor. + Any value provided is passed through the :meth:`norm_idents` method, + which takes care of validating the identifier, + as well as allowing aliases for easier specification + of the identifiers by the user. + + .. todo:: + + document this class's usage + + Class Methods + ============= + .. todo:: document using() and needs_update() options + """ + + #=================================================================== + # class attrs + #=================================================================== + default_ident = None # should be unicode + ident_values = None # should be list of unicode strings + ident_aliases = None # should be dict of unicode -> unicode + # NOTE: any aliases provided to norm_ident() as bytes + # will have been converted to unicode before + # comparing against this dictionary. + + # NOTE: relying on test_06_HasManyIdents() to verify + # these are configured correctly. + + #=================================================================== + # instance attrs + #=================================================================== + ident = None + + #=================================================================== + # variant constructor + #=================================================================== + @classmethod + def using(cls, # keyword only... + default_ident=None, ident=None, **kwds): + """ + This mixin adds support for the following :meth:`~passlib.ifc.PasswordHash.using` keywords: + + :param default_ident: + default identifier that will be used by resulting customized hasher. + + :param ident: + supported as alternate alias for **default_ident**. + """ + # resolve aliases + if ident is not None: + if default_ident is not None: + raise TypeError("'default_ident' and 'ident' are mutually exclusive") + default_ident = ident + + # create subclass + subcls = super(HasManyIdents, cls).using(**kwds) + + # add custom default ident + # (NOTE: creates instance to run value through _norm_ident()) + if default_ident is not None: + subcls.default_ident = cls(ident=default_ident, use_defaults=True).ident + return subcls + + #=================================================================== + # init + #=================================================================== + def __init__(self, ident=None, **kwds): + super(HasManyIdents, self).__init__(**kwds) + + # init ident + if ident is not None: + ident = self._norm_ident(ident) + elif self.use_defaults: + ident = self.default_ident + assert validate_default_value(self, ident, self._norm_ident, param="default_ident") + else: + raise TypeError("no ident specified") + self.ident = ident + + @classmethod + def _norm_ident(cls, ident): + """ + helper which normalizes & validates 'ident' value. + """ + # handle bytes + assert ident is not None + if isinstance(ident, bytes): + ident = ident.decode('ascii') + + # check if identifier is valid + iv = cls.ident_values + if ident in iv: + return ident + + # resolve aliases, and recheck against ident_values + ia = cls.ident_aliases + if ia: + try: + value = ia[ident] + except KeyError: + pass + else: + if value in iv: + return value + + # failure! + # XXX: give this it's own error type? + raise ValueError("invalid ident: %r" % (ident,)) + + #=================================================================== + # password hash api + #=================================================================== + @classmethod + def identify(cls, hash): + hash = to_unicode_for_identify(hash) + return hash.startswith(cls.ident_values) + + @classmethod + def _parse_ident(cls, hash): + """extract ident prefix from hash, helper for subclasses' from_string()""" + hash = to_unicode(hash, "ascii", "hash") + for ident in cls.ident_values: + if hash.startswith(ident): + return ident, hash[len(ident):] + raise exc.InvalidHashError(cls) + + # XXX: implement a needs_update() helper that marks everything but default_ident as deprecated? + + #=================================================================== + # eoc + #=================================================================== + +#------------------------------------------------------------------------ +# salt mixins +#------------------------------------------------------------------------ +class HasSalt(GenericHandler): + """mixin for validating salts. + + This :class:`GenericHandler` mixin adds a ``salt`` keyword to the class constuctor; + any value provided is passed through the :meth:`_norm_salt` method, + which takes care of validating salt length and content, + as well as generating new salts if one it not provided. + + :param salt: + optional salt string + + :param salt_size: + optional size of salt (only used if no salt provided); + defaults to :attr:`default_salt_size`. + + Class Attributes + ================ + In order for :meth:`!_norm_salt` to do its job, the following + attributes should be provided by the handler subclass: + + .. attribute:: min_salt_size + + The minimum number of characters allowed in a salt string. + An :exc:`ValueError` will be throw if the provided salt is too small. + Defaults to ``0``. + + .. attribute:: max_salt_size + + The maximum number of characters allowed in a salt string. + By default an :exc:`ValueError` will be throw if the provided salt is + too large; but if ``relaxed=True``, it will be clipped and a warning + issued instead. Defaults to ``None``, for no maximum. + + .. attribute:: default_salt_size + + [required] + If no salt is provided, this should specify the size of the salt + that will be generated by :meth:`_generate_salt`. By default + this will fall back to :attr:`max_salt_size`. + + .. attribute:: salt_chars + + A string containing all the characters which are allowed in the salt + string. An :exc:`ValueError` will be throw if any other characters + are encountered. May be set to ``None`` to skip this check (but see + in :attr:`default_salt_chars`). + + .. attribute:: default_salt_chars + + [required] + This attribute controls the set of characters use to generate + *new* salt strings. By default, it mirrors :attr:`salt_chars`. + If :attr:`!salt_chars` is ``None``, this attribute must be specified + in order to generate new salts. Aside from that purpose, + the main use of this attribute is for hashes which wish to generate + salts from a restricted subset of :attr:`!salt_chars`; such as + accepting all characters, but only using a-z. + + Instance Attributes + =================== + .. attribute:: salt + + This instance attribute will be filled in with the salt provided + to the constructor (as adapted by :meth:`_norm_salt`) + + Subclassable Methods + ==================== + .. automethod:: _norm_salt + .. automethod:: _generate_salt + """ + # TODO: document _truncate_salt() + # XXX: allow providing raw salt to this class, and encoding it? + + #=================================================================== + # class attrs + #=================================================================== + + min_salt_size = 0 + max_salt_size = None + salt_chars = None + + @classproperty + def default_salt_size(cls): + """default salt size (defaults to *max_salt_size*)""" + return cls.max_salt_size + + @classproperty + def default_salt_chars(cls): + """charset used to generate new salt strings (defaults to *salt_chars*)""" + return cls.salt_chars + + # private helpers for HasRawSalt, shouldn't be used by subclasses + _salt_is_bytes = False + _salt_unit = "chars" + + # TODO: could support using(min/max_desired_salt_size) via using() and needs_update() + + #=================================================================== + # instance attrs + #=================================================================== + salt = None + + #=================================================================== + # variant constructor + #=================================================================== + @classmethod + def using(cls, # keyword only... + default_salt_size=None, + salt_size=None, # aliases used by CryptContext + salt=None, + **kwds): + + # check for aliases used by CryptContext + if salt_size is not None: + if default_salt_size is not None: + raise TypeError("'salt_size' and 'default_salt_size' aliases are mutually exclusive") + default_salt_size = salt_size + + # generate new subclass + subcls = super(HasSalt, cls).using(**kwds) + + # replace default_rounds + relaxed = kwds.get("relaxed") + if default_salt_size is not None: + if isinstance(default_salt_size, native_string_types): + default_salt_size = int(default_salt_size) + subcls.default_salt_size = subcls._clip_to_valid_salt_size(default_salt_size, + param="salt_size", + relaxed=relaxed) + + # if salt specified, replace _generate_salt() with fixed output. + # NOTE: this is mainly useful for testing / debugging. + if salt is not None: + salt = subcls._norm_salt(salt, relaxed=relaxed) + subcls._generate_salt = staticmethod(lambda: salt) + + return subcls + + # XXX: would like to combine w/ _norm_salt() code below, but doesn't quite fit. + @classmethod + def _clip_to_valid_salt_size(cls, salt_size, param="salt_size", relaxed=True): + """ + internal helper -- + clip salt size value to handler's absolute limits (min_salt_size / max_salt_size) + + :param relaxed: + if ``True`` (the default), issues PasslibHashWarning is rounds are outside allowed range. + if ``False``, raises a ValueError instead. + + :param param: + optional name of parameter to insert into error/warning messages. + + :returns: + clipped rounds value + """ + mn = cls.min_salt_size + mx = cls.max_salt_size + + # check if salt size is fixed + if mn == mx: + if salt_size != mn: + msg = "%s: %s (%d) must be exactly %d" % (cls.name, param, salt_size, mn) + if relaxed: + warn(msg, PasslibHashWarning) + else: + raise ValueError(msg) + return mn + + # check min size + if salt_size < mn: + msg = "%s: %s (%r) below min_salt_size (%d)" % (cls.name, param, salt_size, mn) + if relaxed: + warn(msg, PasslibHashWarning) + salt_size = mn + else: + raise ValueError(msg) + + # check max size + if mx and salt_size > mx: + msg = "%s: %s (%r) above max_salt_size (%d)" % (cls.name, param, salt_size, mx) + if relaxed: + warn(msg, PasslibHashWarning) + salt_size = mx + else: + raise ValueError(msg) + + return salt_size + + #=================================================================== + # init + #=================================================================== + def __init__(self, salt=None, **kwds): + super(HasSalt, self).__init__(**kwds) + if salt is not None: + salt = self._parse_salt(salt) + elif self.use_defaults: + salt = self._generate_salt() + assert self._norm_salt(salt) == salt, "generated invalid salt: %r" % (salt,) + else: + raise TypeError("no salt specified") + self.salt = salt + + # NOTE: split out mainly so sha256_crypt can subclass this + def _parse_salt(self, salt): + return self._norm_salt(salt) + + @classmethod + def _norm_salt(cls, salt, relaxed=False): + """helper to normalize & validate user-provided salt string + + :arg salt: + salt string + + :raises TypeError: + If salt not correct type. + + :raises ValueError: + + * if salt contains chars that aren't in :attr:`salt_chars`. + * if salt contains less than :attr:`min_salt_size` characters. + * if ``relaxed=False`` and salt has more than :attr:`max_salt_size` + characters (if ``relaxed=True``, the salt is truncated + and a warning is issued instead). + + :returns: + normalized salt + """ + # check type + if cls._salt_is_bytes: + if not isinstance(salt, bytes): + raise exc.ExpectedTypeError(salt, "bytes", "salt") + else: + if not isinstance(salt, unicode): + # NOTE: allowing bytes under py2 so salt can be native str. + if isinstance(salt, bytes) and (PY2 or relaxed): + salt = salt.decode("ascii") + else: + raise exc.ExpectedTypeError(salt, "unicode", "salt") + + # check charset + sc = cls.salt_chars + if sc is not None and any(c not in sc for c in salt): + raise ValueError("invalid characters in %s salt" % cls.name) + + # check min size + mn = cls.min_salt_size + if mn and len(salt) < mn: + msg = "salt too small (%s requires %s %d %s)" % (cls.name, + "exactly" if mn == cls.max_salt_size else ">=", mn, cls._salt_unit) + raise ValueError(msg) + + # check max size + mx = cls.max_salt_size + if mx and len(salt) > mx: + msg = "salt too large (%s requires %s %d %s)" % (cls.name, + "exactly" if mx == mn else "<=", mx, cls._salt_unit) + if relaxed: + warn(msg, PasslibHashWarning) + salt = cls._truncate_salt(salt, mx) + else: + raise ValueError(msg) + + return salt + + @staticmethod + def _truncate_salt(salt, mx): + # NOTE: some hashes (e.g. bcrypt) has structure within their + # salt string. this provides a method to override to perform + # the truncation properly + return salt[:mx] + + @classmethod + def _generate_salt(cls): + """ + helper method for _init_salt(); generates a new random salt string. + """ + return getrandstr(rng, cls.default_salt_chars, cls.default_salt_size) + + @classmethod + def bitsize(cls, salt_size=None, **kwds): + """[experimental method] return info about bitsizes of hash""" + info = super(HasSalt, cls).bitsize(**kwds) + if salt_size is None: + salt_size = cls.default_salt_size + # FIXME: this may overestimate size due to padding bits + # FIXME: this will be off by 1 for case-insensitive hashes. + info['salt'] = _bitsize(salt_size, cls.default_salt_chars) + return info + + #=================================================================== + # eoc + #=================================================================== + +class HasRawSalt(HasSalt): + """mixin for classes which use decoded salt parameter + + A variant of :class:`!HasSalt` which takes in decoded bytes instead of an encoded string. + + .. todo:: + + document this class's usage + """ + + salt_chars = ALL_BYTE_VALUES + + # NOTE: all HasRawSalt code is currently part of HasSalt, using private + # '_salt_is_bytes' flag. this arrangement may be changed in the future. + _salt_is_bytes = True + _salt_unit = "bytes" + + @classmethod + def _generate_salt(cls): + assert cls.salt_chars in [None, ALL_BYTE_VALUES] + return getrandbytes(rng, cls.default_salt_size) + +#------------------------------------------------------------------------ +# rounds mixin +#------------------------------------------------------------------------ +class HasRounds(GenericHandler): + """mixin for validating rounds parameter + + This :class:`GenericHandler` mixin adds a ``rounds`` keyword to the class + constuctor; any value provided is passed through the :meth:`_norm_rounds` + method, which takes care of validating the number of rounds. + + :param rounds: optional number of rounds hash should use + + Class Attributes + ================ + In order for :meth:`!_norm_rounds` to do its job, the following + attributes must be provided by the handler subclass: + + .. attribute:: min_rounds + + The minimum number of rounds allowed. A :exc:`ValueError` will be + thrown if the rounds value is too small. Defaults to ``0``. + + .. attribute:: max_rounds + + The maximum number of rounds allowed. A :exc:`ValueError` will be + thrown if the rounds value is larger than this. Defaults to ``None`` + which indicates no limit to the rounds value. + + .. attribute:: default_rounds + + If no rounds value is provided to constructor, this value will be used. + If this is not specified, a rounds value *must* be specified by the + application. + + .. attribute:: rounds_cost + + [required] + The ``rounds`` parameter typically encodes a cpu-time cost + for calculating a hash. This should be set to ``"linear"`` + (the default) or ``"log2"``, depending on how the rounds value relates + to the actual amount of time that will be required. + + Class Methods + ============= + .. todo:: document using() and needs_update() options + + Instance Attributes + =================== + .. attribute:: rounds + + This instance attribute will be filled in with the rounds value provided + to the constructor (as adapted by :meth:`_norm_rounds`) + + Subclassable Methods + ==================== + .. automethod:: _norm_rounds + """ + #=================================================================== + # class attrs + #=================================================================== + + #----------------- + # algorithm options -- not application configurable + #----------------- + # XXX: rename to min_valid_rounds / max_valid_rounds, + # to clarify role compared to min_desired_rounds / max_desired_rounds? + min_rounds = 0 + max_rounds = None + rounds_cost = "linear" # default to the common case + + # hack to pass info to _CryptRecord (will be removed in passlib 2.0) + using_rounds_kwds = ("min_desired_rounds", "max_desired_rounds", + "min_rounds", "max_rounds", + "default_rounds", "vary_rounds") + + #----------------- + # desired & default rounds -- configurable via .using() classmethod + #----------------- + min_desired_rounds = None + max_desired_rounds = None + default_rounds = None + vary_rounds = None + + #=================================================================== + # instance attrs + #=================================================================== + rounds = None + + #=================================================================== + # variant constructor + #=================================================================== + @classmethod + def using(cls, # keyword only... + min_desired_rounds=None, max_desired_rounds=None, + default_rounds=None, vary_rounds=None, + min_rounds=None, max_rounds=None, rounds=None, # aliases used by CryptContext + **kwds): + + # check for aliases used by CryptContext + if min_rounds is not None: + if min_desired_rounds is not None: + raise TypeError("'min_rounds' and 'min_desired_rounds' aliases are mutually exclusive") + min_desired_rounds = min_rounds + + if max_rounds is not None: + if max_desired_rounds is not None: + raise TypeError("'max_rounds' and 'max_desired_rounds' aliases are mutually exclusive") + max_desired_rounds = max_rounds + + # use 'rounds' as fallback for min, max, AND default + # XXX: would it be better to make 'default_rounds' and 'rounds' + # aliases, and have a separate 'require_rounds' parameter for this behavior? + if rounds is not None: + if min_desired_rounds is None: + min_desired_rounds = rounds + if max_desired_rounds is None: + max_desired_rounds = rounds + if default_rounds is None: + default_rounds = rounds + + # generate new subclass + subcls = super(HasRounds, cls).using(**kwds) + + # replace min_desired_rounds + relaxed = kwds.get("relaxed") + if min_desired_rounds is None: + explicit_min_rounds = False + min_desired_rounds = cls.min_desired_rounds + else: + explicit_min_rounds = True + if isinstance(min_desired_rounds, native_string_types): + min_desired_rounds = int(min_desired_rounds) + subcls.min_desired_rounds = subcls._norm_rounds(min_desired_rounds, + param="min_desired_rounds", + relaxed=relaxed) + + # replace max_desired_rounds + if max_desired_rounds is None: + max_desired_rounds = cls.max_desired_rounds + else: + if isinstance(max_desired_rounds, native_string_types): + max_desired_rounds = int(max_desired_rounds) + if min_desired_rounds and max_desired_rounds < min_desired_rounds: + msg = "%s: max_desired_rounds (%r) below min_desired_rounds (%r)" % \ + (subcls.name, max_desired_rounds, min_desired_rounds) + if explicit_min_rounds: + raise ValueError(msg) + else: + warn(msg, PasslibConfigWarning) + max_desired_rounds = min_desired_rounds + subcls.max_desired_rounds = subcls._norm_rounds(max_desired_rounds, + param="max_desired_rounds", + relaxed=relaxed) + + # replace default_rounds + if default_rounds is not None: + if isinstance(default_rounds, native_string_types): + default_rounds = int(default_rounds) + if min_desired_rounds and default_rounds < min_desired_rounds: + raise ValueError("%s: default_rounds (%r) below min_desired_rounds (%r)" % + (subcls.name, default_rounds, min_desired_rounds)) + elif max_desired_rounds and default_rounds > max_desired_rounds: + raise ValueError("%s: default_rounds (%r) above max_desired_rounds (%r)" % + (subcls.name, default_rounds, max_desired_rounds)) + subcls.default_rounds = subcls._norm_rounds(default_rounds, + param="default_rounds", + relaxed=relaxed) + + # clip default rounds to new limits. + if subcls.default_rounds is not None: + subcls.default_rounds = subcls._clip_to_desired_rounds(subcls.default_rounds) + + # replace / set vary_rounds + if vary_rounds is not None: + if isinstance(vary_rounds, native_string_types): + if vary_rounds.endswith("%"): + vary_rounds = float(vary_rounds[:-1]) * 0.01 + elif "." in vary_rounds: + vary_rounds = float(vary_rounds) + else: + vary_rounds = int(vary_rounds) + if vary_rounds < 0: + raise ValueError("%s: vary_rounds (%r) below 0" % + (subcls.name, vary_rounds)) + elif isinstance(vary_rounds, float): + # TODO: deprecate / disallow vary_rounds=1.0 + if vary_rounds > 1: + raise ValueError("%s: vary_rounds (%r) above 1.0" % + (subcls.name, vary_rounds)) + elif not isinstance(vary_rounds, int): + raise TypeError("vary_rounds must be int or float") + if vary_rounds: + warn("The 'vary_rounds' option is deprecated as of Passlib 1.7, " + "and will be removed in Passlib 2.0", PasslibConfigWarning) + subcls.vary_rounds = vary_rounds + # XXX: could cache _calc_vary_rounds_range() here if needed, + # but would need to handle user manually changing .default_rounds + return subcls + + @classmethod + def _clip_to_desired_rounds(cls, rounds): + """ + helper for :meth:`_generate_rounds` -- + clips rounds value to desired min/max set by class (if any) + """ + # NOTE: min/max_desired_rounds are None if unset. + # check minimum + mnd = cls.min_desired_rounds or 0 + if rounds < mnd: + return mnd + + # check maximum + mxd = cls.max_desired_rounds + if mxd and rounds > mxd: + return mxd + + return rounds + + @classmethod + def _calc_vary_rounds_range(cls, default_rounds): + """ + helper for :meth:`_generate_rounds` -- + returns range for vary rounds generation. + + :returns: + (lower, upper) limits suitable for random.randint() + """ + # XXX: could precalculate output of this in using() method, and save per-hash cost. + # but then users patching cls.vary_rounds / cls.default_rounds would get wrong value. + assert default_rounds + vary_rounds = cls.vary_rounds + + # if vary_rounds specified as % of default, convert it to actual rounds + def linear_to_native(value, upper): + return value + if isinstance(vary_rounds, float): + assert 0 <= vary_rounds <= 1 # TODO: deprecate vary_rounds==1 + if cls.rounds_cost == "log2": + # special case -- have to convert default_rounds to linear scale, + # apply +/- vary_rounds to that, and convert back to log scale again. + # linear_to_native() takes care of the "convert back" step. + default_rounds = 1 << default_rounds + def linear_to_native(value, upper): + if value <= 0: # log() undefined for <= 0 + return 0 + elif upper: # use smallest upper bound for start of range + return int(math.log(value, 2)) + else: # use greatest lower bound for end of range + return int(math.ceil(math.log(value, 2))) + # calculate integer vary rounds based on current default_rounds + vary_rounds = int(default_rounds * vary_rounds) + + # calculate bounds based on default_rounds +/- vary_rounds + assert vary_rounds >= 0 and isinstance(vary_rounds, int_types) + lower = linear_to_native(default_rounds - vary_rounds, False) + upper = linear_to_native(default_rounds + vary_rounds, True) + return cls._clip_to_desired_rounds(lower), cls._clip_to_desired_rounds(upper) + + #=================================================================== + # init + #=================================================================== + def __init__(self, rounds=None, **kwds): + super(HasRounds, self).__init__(**kwds) + if rounds is not None: + rounds = self._parse_rounds(rounds) + elif self.use_defaults: + rounds = self._generate_rounds() + assert self._norm_rounds(rounds) == rounds, "generated invalid rounds: %r" % (rounds,) + else: + raise TypeError("no rounds specified") + self.rounds = rounds + + # NOTE: split out mainly so sha256_crypt & bsdi_crypt can subclass this + def _parse_rounds(self, rounds): + return self._norm_rounds(rounds) + + @classmethod + def _norm_rounds(cls, rounds, relaxed=False, param="rounds"): + """ + helper for normalizing rounds value. + + :arg rounds: + an integer cost parameter. + + :param relaxed: + if ``True`` (the default), issues PasslibHashWarning is rounds are outside allowed range. + if ``False``, raises a ValueError instead. + + :param param: + optional name of parameter to insert into error/warning messages. + + :raises TypeError: + * if ``use_defaults=False`` and no rounds is specified + * if rounds is not an integer. + + :raises ValueError: + + * if rounds is ``None`` and class does not specify a value for + :attr:`default_rounds`. + * if ``relaxed=False`` and rounds is outside bounds of + :attr:`min_rounds` and :attr:`max_rounds` (if ``relaxed=True``, + the rounds value will be clamped, and a warning issued). + + :returns: + normalized rounds value + """ + return norm_integer(cls, rounds, cls.min_rounds, cls.max_rounds, + param=param, relaxed=relaxed) + + @classmethod + def _generate_rounds(cls): + """ + internal helper for :meth:`_norm_rounds` -- + returns default rounds value, incorporating vary_rounds, + and any other limitations hash may place on rounds parameter. + """ + # load default rounds + rounds = cls.default_rounds + if rounds is None: + raise TypeError("%s rounds value must be specified explicitly" % (cls.name,)) + + # randomly vary the rounds slightly basic on vary_rounds parameter. + # reads default_rounds internally. + if cls.vary_rounds: + lower, upper = cls._calc_vary_rounds_range(rounds) + assert lower <= rounds <= upper + if lower < upper: + rounds = rng.randint(lower, upper) + + return rounds + + #=================================================================== + # migration interface + #=================================================================== + def _calc_needs_update(self, **kwds): + """ + mark hash as needing update if rounds is outside desired bounds. + """ + min_desired_rounds = self.min_desired_rounds + if min_desired_rounds and self.rounds < min_desired_rounds: + return True + max_desired_rounds = self.max_desired_rounds + if max_desired_rounds and self.rounds > max_desired_rounds: + return True + return super(HasRounds, self)._calc_needs_update(**kwds) + + #=================================================================== + # experimental methods + #=================================================================== + @classmethod + def bitsize(cls, rounds=None, vary_rounds=.1, **kwds): + """[experimental method] return info about bitsizes of hash""" + info = super(HasRounds, cls).bitsize(**kwds) + # NOTE: this essentially estimates how many bits of "salt" + # can be added by varying the rounds value just a little bit. + if cls.rounds_cost != "log2": + # assume rounds can be randomized within the range + # rounds*(1-vary_rounds) ... rounds*(1+vary_rounds) + # then this can be used to encode + # log2(rounds*(1+vary_rounds)-rounds*(1-vary_rounds)) + # worth of salt-like bits. this works out to + # 1+log2(rounds*vary_rounds) + import math + if rounds is None: + rounds = cls.default_rounds + info['rounds'] = max(0, int(1+math.log(rounds*vary_rounds,2))) + ## else: # log2 rounds + # all bits of the rounds value are critical to choosing + # the time-cost, and can't be randomized. + return info + + #=================================================================== + # eoc + #=================================================================== + +#------------------------------------------------------------------------ +# other common parameters +#------------------------------------------------------------------------ +class ParallelismMixin(GenericHandler): + """ + mixin which provides common behavior for 'parallelism' setting + """ + #=================================================================== + # class attrs + #=================================================================== + + # NOTE: subclasses should add "parallelism" to their settings_kwds + + #=================================================================== + # instance attrs + #=================================================================== + + #: parallelism setting (class-level value used as default) + parallelism = 1 + + #=================================================================== + # variant constructor + #=================================================================== + + @classmethod + def using(cls, parallelism=None, **kwds): + subcls = super(ParallelismMixin, cls).using(**kwds) + if parallelism is not None: + if isinstance(parallelism, native_string_types): + parallelism = int(parallelism) + subcls.parallelism = subcls._norm_parallelism(parallelism, relaxed=kwds.get("relaxed")) + return subcls + + #=================================================================== + # init + #=================================================================== + def __init__(self, parallelism=None, **kwds): + super(ParallelismMixin, self).__init__(**kwds) + + # init parallelism + if parallelism is None: + assert validate_default_value(self, self.parallelism, self._norm_parallelism, + param="parallelism") + else: + self.parallelism = self._norm_parallelism(parallelism) + + @classmethod + def _norm_parallelism(cls, parallelism, relaxed=False): + return norm_integer(cls, parallelism, min=1, param="parallelism", relaxed=relaxed) + + #=================================================================== + # hash migration + #=================================================================== + + def _calc_needs_update(self, **kwds): + """ + mark hash as needing update if rounds is outside desired bounds. + """ + # XXX: for now, marking all hashes which don't have matching parallelism setting + if self.parallelism != type(self).parallelism: + return True + return super(ParallelismMixin, self)._calc_needs_update(**kwds) + + #=================================================================== + # eoc + #=================================================================== + +#------------------------------------------------------------------------ +# backend mixin & helpers +#------------------------------------------------------------------------ + +#: global lock that must be held when changing backends. +#: not bothering to make this more granular, as backend switching +#: isn't a speed-critical path. lock is needed since there is some +#: class-level state that may be modified during a "dry run" +_backend_lock = threading.RLock() + +class BackendMixin(PasswordHash): + """ + PasswordHash mixin which provides generic framework for supporting multiple backends + within the class. + + Public API + ---------- + + .. attribute:: backends + + This attribute should be a tuple containing the names of the backends + which are supported. Two common names are ``"os_crypt"`` (if backend + uses :mod:`crypt`), and ``"builtin"`` (if the backend is a pure-python + fallback). + + .. automethod:: get_backend + .. automethod:: set_backend + .. automethod:: has_backend + + .. warning:: + + :meth:`set_backend` is intended to be called during application startup -- + it affects global state, and switching backends is not guaranteed threadsafe. + + Private API (Subclass Hooks) + ---------------------------- + Subclasses should set the :attr:`!backends` attribute to a tuple of the backends + they wish to support. They should also define one method: + + .. classmethod:: _load_backend_{name}(dryrun=False) + + One copy of this method should be defined for each :samp:`name` within :attr:`!backends`. + + It will be called in order to load the backend, and should take care of whatever + is needed to enable the backend. This may include importing modules, running tests, + issuing warnings, etc. + + :param name: + [Optional] name of backend. + + :param dryrun: + [Optional] True/False if currently performing a "dry run". + + if True, the method should perform all setup actions *except* + switching the class over to the new backend. + + :raises passlib.exc.PasslibSecurityError: + if the backend is available, but cannot be loaded due to a security issue. + + :returns: + False if backend not available, True if backend loaded. + + .. warning:: + + Due to the way passlib's internals are arranged, + backends should generally store stateful data at the class level + (not the module level), and be prepared to be called on subclasses + which may be set to a different backend from their parent. + + (Idempotent module-level data such as lazy imports are fine). + + .. automethod:: _finalize_backend + + .. versionadded:: 1.7 + """ + #=================================================================== + # class attrs + #=================================================================== + + #: list of backend names, provided by subclass. + backends = None + + #: private attr mixin uses to hold currently loaded backend (or ``None``) + __backend = None + + #: optional class-specific text containing suggestion about what to do + #: when no backends are available. + _no_backend_suggestion = None + + #: shared attr used by set_backend() to indicate what backend it's loaded; + #: meaningless while not in set_backend(). + _pending_backend = None + + #: shared attr used by set_backend() to indicate if it's in "dry run" mode; + #: meaningless while not in set_backend(). + _pending_dry_run = False + + #=================================================================== + # public api + #=================================================================== + + @classmethod + def get_backend(cls): + """ + Return name of currently active backend. + if no backend has been loaded, loads and returns name of default backend. + + :raises passlib.exc.MissingBackendError: + if no backends are available. + + :returns: + name of active backend + """ + if not cls.__backend: + cls.set_backend() + assert cls.__backend, "set_backend() failed to load a default backend" + return cls.__backend + + @classmethod + def has_backend(cls, name="any"): + """ + Check if support is currently available for specified backend. + + :arg name: + name of backend to check for. + can be any string accepted by :meth:`set_backend`. + + :raises ValueError: + if backend name is unknown + + :returns: + * ``True`` if backend is available. + * ``False`` if it's available / can't be loaded. + * ``None`` if it's present, but won't load due to a security issue. + """ + try: + cls.set_backend(name, dryrun=True) + return True + except (exc.MissingBackendError, exc.PasslibSecurityError): + return False + + @classmethod + def set_backend(cls, name="any", dryrun=False): + """ + Load specified backend. + + :arg name: + name of backend to load, can be any of the following: + + * ``"any"`` -- use current backend if one is loaded, + otherwise load the first available backend. + + * ``"default"`` -- use the first available backend. + + * any string in :attr:`backends`, loads specified backend. + + :param dryrun: + If True, this perform all setup actions *except* switching over to the new backend. + (this flag is used to implement :meth:`has_backend`). + + .. versionadded:: 1.7 + + :raises ValueError: + If backend name is unknown. + + :raises passlib.exc.MissingBackendError: + If specific backend is missing; + or in the case of ``"any"`` / ``"default"``, if *no* backends are available. + + :raises passlib.exc.PasslibSecurityError: + + If ``"any"`` or ``"default"`` was specified, + but the only backend available has a PasslibSecurityError. + """ + # check if active backend is acceptable + if (name == "any" and cls.__backend) or (name and name == cls.__backend): + return cls.__backend + + # if this isn't the final subclass, whose bases we can modify, + # find that class, and recursively call this method for the proper class. + owner = cls._get_backend_owner() + if owner is not cls: + return owner.set_backend(name, dryrun=dryrun) + + # pick first available backend + if name == "any" or name == "default": + default_error = None + for name in cls.backends: + try: + return cls.set_backend(name, dryrun=dryrun) + except exc.MissingBackendError: + continue + except exc.PasslibSecurityError as err: + # backend is available, but refuses to load due to security issue. + if default_error is None: + default_error = err + continue + if default_error is None: + msg = "%s: no backends available" % cls.name + if cls._no_backend_suggestion: + msg += cls._no_backend_suggestion + default_error = exc.MissingBackendError(msg) + raise default_error + + # validate name + if name not in cls.backends: + raise exc.UnknownBackendError(cls, name) + + # hand off to _set_backend() + with _backend_lock: + orig = cls._pending_backend, cls._pending_dry_run + try: + cls._pending_backend = name + cls._pending_dry_run = dryrun + cls._set_backend(name, dryrun) + finally: + cls._pending_backend, cls._pending_dry_run = orig + if not dryrun: + cls.__backend = name + return name + + #=================================================================== + # subclass hooks + #=================================================================== + + @classmethod + def _get_backend_owner(cls): + """ + return class that set_backend() should actually be modifying. + for SubclassBackendMixin, this may not always be the class that was invoked. + """ + return cls + + @classmethod + def _set_backend(cls, name, dryrun): + """ + Internal method invoked by :meth:`set_backend`. + handles actual loading of specified backend. + + global _backend_lock will be held for duration of this method, + and _pending_dry_run & _pending_backend will also be set. + + should return True / False. + """ + loader = cls._get_backend_loader(name) + kwds = {} + if accepts_keyword(loader, "name"): + kwds['name'] = name + if accepts_keyword(loader, "dryrun"): + kwds['dryrun'] = dryrun + ok = loader(**kwds) + if ok is False: + raise exc.MissingBackendError("%s: backend not available: %s" % + (cls.name, name)) + elif ok is not True: + raise AssertionError("backend loaders must return True or False" + ": %r" % (ok,)) + + @classmethod + def _get_backend_loader(cls, name): + """ + Hook called to get the specified backend's loader. + Should return callable which optionally takes ``"name"`` and/or + ``"dryrun"`` keywords. + + Callable should return True if backend initialized successfully. + + If backend can't be loaded, callable should return False + OR raise MissingBackendError directly. + """ + raise NotImplementedError("implement in subclass") + + @classmethod + def _stub_requires_backend(cls): + """ + helper for subclasses to create stub methods which auto-load backend. + """ + if cls.__backend: + raise AssertionError("%s: _finalize_backend(%r) failed to replace lazy loader" % + (cls.name, cls.__backend)) + cls.set_backend() + if not cls.__backend: + raise AssertionError("%s: set_backend() failed to load a default backend" % + (cls.name)) + + #=================================================================== + # eoc + #=================================================================== + +class SubclassBackendMixin(BackendMixin): + """ + variant of BackendMixin which allows backends to be implemented + as separate mixin classes, and dynamically switches them out. + + backend classes should implement a _load_backend() classmethod, + which will be invoked with an optional 'dryrun' keyword, + and should return True or False. + + _load_backend() will be invoked with ``cls`` equal to the mixin, + *not* the overall class. + + .. versionadded:: 1.7 + """ + #=================================================================== + # class attrs + #=================================================================== + + # 'backends' required by BackendMixin + + #: NON-INHERITED flag that this class's bases should be modified by SubclassBackendMixin. + #: should only be set to True in *one* subclass in hierarchy. + _backend_mixin_target = False + + #: map of backend name -> mixin class + _backend_mixin_map = None + + #=================================================================== + # backend loading + #=================================================================== + + @classmethod + def _get_backend_owner(cls): + """ + return base class that we're actually switching backends on + (needed in since backends frequently modify class attrs, + and .set_backend may be called from a subclass). + """ + if not cls._backend_mixin_target: + raise AssertionError("_backend_mixin_target not set") + for base in cls.__mro__: + if base.__dict__.get("_backend_mixin_target"): + return base + raise AssertionError("expected to find class w/ '_backend_mixin_target' set") + + @classmethod + def _set_backend(cls, name, dryrun): + # invoke backend loader (will throw error if fails) + super(SubclassBackendMixin, cls)._set_backend(name, dryrun) + + # sanity check call args (should trust .set_backend, but will really + # foul things up if this isn't the owner) + assert cls is cls._get_backend_owner(), "_finalize_backend() not invoked on owner" + + # pick mixin class + mixin_map = cls._backend_mixin_map + assert mixin_map, "_backend_mixin_map not specified" + mixin_cls = mixin_map[name] + assert issubclass(mixin_cls, SubclassBackendMixin), "invalid mixin class" + + # modify to remove existing backend mixins, and insert the new one + update_mixin_classes(cls, + add=mixin_cls, + remove=mixin_map.values(), + append=True, before=SubclassBackendMixin, + dryrun=dryrun, + ) + + @classmethod + def _get_backend_loader(cls, name): + assert cls._backend_mixin_map, "_backend_mixin_map not specified" + return cls._backend_mixin_map[name]._load_backend_mixin + + #=================================================================== + # eoc + #=================================================================== + +# XXX: rename to ChecksumBackendMixin? +class HasManyBackends(BackendMixin, GenericHandler): + """ + GenericHandler mixin which provides selecting from multiple backends. + + .. todo:: + + finish documenting this class's usage + + For hashes which need to select from multiple backends, + depending on the host environment, this class + offers a way to specify alternate :meth:`_calc_checksum` methods, + and will dynamically chose the best one at runtime. + + .. versionchanged:: 1.7 + + This class now derives from :class:`BackendMixin`, which abstracts + out a more generic framework for supporting multiple backends. + The public api (:meth:`!get_backend`, :meth:`!has_backend`, :meth:`!set_backend`) + is roughly the same. + + Private API (Subclass Hooks) + ---------------------------- + As of version 1.7, classes should implement :meth:`!_load_backend_{name}`, per + :class:`BackendMixin`. This hook should invoke :meth:`!_set_calc_checksum_backcend` + to install it's backend method. + + .. deprecated:: 1.7 + + The following api is deprecated, and will be removed in Passlib 2.0: + + .. attribute:: _has_backend_{name} + + private class attribute checked by :meth:`has_backend` to see if a + specific backend is available, it should be either ``True`` + or ``False``. One of these should be provided by + the subclass for each backend listed in :attr:`backends`. + + .. classmethod:: _calc_checksum_{name} + + private class method that should implement :meth:`_calc_checksum` + for a given backend. it will only be called if the backend has + been selected by :meth:`set_backend`. One of these should be provided + by the subclass for each backend listed in :attr:`backends`. + """ + #=================================================================== + # digest calculation + #=================================================================== + + def _calc_checksum(self, secret): + "wrapper for backend, for common code""" + # NOTE: not overwriting _calc_checksum() directly, so that classes can provide + # common behavior in that method, + # and then invoke _calc_checksum_backend() to do the work. + return self._calc_checksum_backend(secret) + + def _calc_checksum_backend(self, secret): + """ + stub for _calc_checksum_backend() -- + should load backend if one hasn't been loaded; + if one has been loaded, this method should have been monkeypatched by _finalize_backend(). + """ + self._stub_requires_backend() + return self._calc_checksum_backend(secret) + + #=================================================================== + # BackendMixin hooks + #=================================================================== + @classmethod + def _get_backend_loader(cls, name): + """ + subclassed to support legacy 1.6 HasManyBackends api. + (will be removed in passlib 2.0) + """ + # check for 1.7 loader + loader = getattr(cls, "_load_backend_" + name, None) + if loader is None: + # fallback to pre-1.7 _has_backend_xxx + _calc_checksum_xxx() api + def loader(): + return cls.__load_legacy_backend(name) + else: + # make sure 1.6 api isn't defined at same time + assert not hasattr(cls, "_has_backend_" + name), ( + "%s: can't specify both ._load_backend_%s() " + "and ._has_backend_%s" % (cls.name, name, name) + ) + return loader + + @classmethod + def __load_legacy_backend(cls, name): + value = getattr(cls, "_has_backend_" + name) + warn("%s: support for ._has_backend_%s is deprecated as of Passlib 1.7, " + "and will be removed in Passlib 1.9/2.0, please implement " + "._load_backend_%s() instead" % (cls.name, name, name), + DeprecationWarning, + ) + if value: + func = getattr(cls, "_calc_checksum_" + name) + cls._set_calc_checksum_backend(func) + return True + else: + return False + + @classmethod + def _set_calc_checksum_backend(cls, func): + """ + helper used by subclasses to validate & set backend-specific + calc checksum helper. + """ + backend = cls._pending_backend + assert backend, "should only be called during set_backend()" + if not callable(func): + raise RuntimeError("%s: backend %r returned invalid callable: %r" % + (cls.name, backend, func)) + if not cls._pending_dry_run: + cls._calc_checksum_backend = func + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# wrappers +#============================================================================= +# XXX: should this inherit from PasswordHash? +class PrefixWrapper(object): + """wraps another handler, adding a constant prefix. + + instances of this class wrap another password hash handler, + altering the constant prefix that's prepended to the wrapped + handlers' hashes. + + this is used mainly by the :doc:`ldap crypt ` handlers; + such as :class:`~passlib.hash.ldap_md5_crypt` which wraps :class:`~passlib.hash.md5_crypt` and adds a ``{CRYPT}`` prefix. + + usage:: + + myhandler = PrefixWrapper("myhandler", "md5_crypt", prefix="$mh$", orig_prefix="$1$") + + :param name: name to assign to handler + :param wrapped: handler object or name of registered handler + :param prefix: identifying prefix to prepend to all hashes + :param orig_prefix: prefix to strip (defaults to ''). + :param lazy: if True and wrapped handler is specified by name, don't look it up until needed. + """ + + #: list of attributes which should be cloned by .using() + _using_clone_attrs = () + + def __init__(self, name, wrapped, prefix=u(''), orig_prefix=u(''), lazy=False, + doc=None, ident=None): + self.name = name + if isinstance(prefix, bytes): + prefix = prefix.decode("ascii") + self.prefix = prefix + if isinstance(orig_prefix, bytes): + orig_prefix = orig_prefix.decode("ascii") + self.orig_prefix = orig_prefix + if doc: + self.__doc__ = doc + if hasattr(wrapped, "name"): + self._set_wrapped(wrapped) + else: + self._wrapped_name = wrapped + if not lazy: + self._get_wrapped() + + if ident is not None: + if ident is True: + # signal that prefix is identifiable in itself. + if prefix: + ident = prefix + else: + raise ValueError("no prefix specified") + if isinstance(ident, bytes): + ident = ident.decode("ascii") + # XXX: what if ident includes parts of wrapped hash's ident? + if ident[:len(prefix)] != prefix[:len(ident)]: + raise ValueError("ident must agree with prefix") + self._ident = ident + + _wrapped_name = None + _wrapped_handler = None + + def _set_wrapped(self, handler): + # check this is a valid handler + if 'ident' in handler.setting_kwds and self.orig_prefix: + # TODO: look into way to fix the issues. + warn("PrefixWrapper: 'orig_prefix' option may not work correctly " + "for handlers which have multiple identifiers: %r" % + (handler.name,), exc.PasslibRuntimeWarning) + + # store reference + self._wrapped_handler = handler + + def _get_wrapped(self): + handler = self._wrapped_handler + if handler is None: + handler = get_crypt_handler(self._wrapped_name) + self._set_wrapped(handler) + return handler + + wrapped = property(_get_wrapped) + + _ident = False + + @property + def ident(self): + value = self._ident + if value is False: + value = None + # XXX: how will this interact with orig_prefix ? + # not exposing attrs for now if orig_prefix is set. + if not self.orig_prefix: + wrapped = self.wrapped + ident = getattr(wrapped, "ident", None) + if ident is not None: + value = self._wrap_hash(ident) + self._ident = value + return value + + _ident_values = False + + @property + def ident_values(self): + value = self._ident_values + if value is False: + value = None + # XXX: how will this interact with orig_prefix ? + # not exposing attrs for now if orig_prefix is set. + if not self.orig_prefix: + wrapped = self.wrapped + idents = getattr(wrapped, "ident_values", None) + if idents: + value = tuple(self._wrap_hash(ident) for ident in idents) + ##else: + ## ident = self.ident + ## if ident is not None: + ## value = [ident] + self._ident_values = value + return value + + # attrs that should be proxied + # XXX: change this to proxy everything that doesn't start with "_"? + _proxy_attrs = ( + "setting_kwds", "context_kwds", + "default_rounds", "min_rounds", "max_rounds", "rounds_cost", + "min_desired_rounds", "max_desired_rounds", "vary_rounds", + "default_salt_size", "min_salt_size", "max_salt_size", + "salt_chars", "default_salt_chars", + "backends", "has_backend", "get_backend", "set_backend", + "is_disabled", "truncate_size", "truncate_error", + "truncate_verify_reject", + + # internal info attrs needed for test inspection + "_salt_is_bytes", + ) + + def __repr__(self): + args = [ repr(self._wrapped_name or self._wrapped_handler) ] + if self.prefix: + args.append("prefix=%r" % self.prefix) + if self.orig_prefix: + args.append("orig_prefix=%r" % self.orig_prefix) + args = ", ".join(args) + return 'PrefixWrapper(%r, %s)' % (self.name, args) + + def __dir__(self): + attrs = set(dir(self.__class__)) + attrs.update(self.__dict__) + wrapped = self.wrapped + attrs.update( + attr for attr in self._proxy_attrs + if hasattr(wrapped, attr) + ) + return list(attrs) + + def __getattr__(self, attr): + """proxy most attributes from wrapped class (e.g. rounds, salt size, etc)""" + if attr in self._proxy_attrs: + return getattr(self.wrapped, attr) + raise AttributeError("missing attribute: %r" % (attr,)) + + def __setattr__(self, attr, value): + # if proxy attr present on wrapped object, + # and we own it, modify *it* instead. + # TODO: needs UTs + # TODO: any other cases where wrapped is "owned"? + # currently just if created via .using() + if attr in self._proxy_attrs and self._derived_from: + wrapped = self.wrapped + if hasattr(wrapped, attr): + setattr(wrapped, attr, value) + return + return object.__setattr__(self, attr, value) + + def _unwrap_hash(self, hash): + """given hash belonging to wrapper, return orig version""" + # NOTE: assumes hash has been validated as unicode already + prefix = self.prefix + if not hash.startswith(prefix): + raise exc.InvalidHashError(self) + # NOTE: always passing to handler as unicode, to save reconversion + return self.orig_prefix + hash[len(prefix):] + + def _wrap_hash(self, hash): + """given orig hash; return one belonging to wrapper""" + # NOTE: should usually be native string. + # (which does mean extra work under py2, but not py3) + if isinstance(hash, bytes): + hash = hash.decode("ascii") + orig_prefix = self.orig_prefix + if not hash.startswith(orig_prefix): + raise exc.InvalidHashError(self.wrapped) + wrapped = self.prefix + hash[len(orig_prefix):] + return uascii_to_str(wrapped) + + #: set by _using(), helper for test harness' handler_derived_from() + _derived_from = None + + def using(self, **kwds): + # generate subclass of wrapped handler + subcls = self.wrapped.using(**kwds) + assert subcls is not self.wrapped + # then create identical wrapper which wraps the new subclass. + wrapper = PrefixWrapper(self.name, subcls, prefix=self.prefix, orig_prefix=self.orig_prefix) + wrapper._derived_from = self + for attr in self._using_clone_attrs: + setattr(wrapper, attr, getattr(self, attr)) + return wrapper + + def needs_update(self, hash, **kwds): + hash = self._unwrap_hash(hash) + return self.wrapped.needs_update(hash, **kwds) + + def identify(self, hash): + hash = to_unicode_for_identify(hash) + if not hash.startswith(self.prefix): + return False + hash = self._unwrap_hash(hash) + return self.wrapped.identify(hash) + + @deprecated_method(deprecated="1.7", removed="2.0") + def genconfig(self, **kwds): + config = self.wrapped.genconfig(**kwds) + if config is None: + raise RuntimeError(".genconfig() must return a string, not None") + return self._wrap_hash(config) + + @deprecated_method(deprecated="1.7", removed="2.0") + def genhash(self, secret, config, **kwds): + # TODO: under 2.0, throw TypeError if config is None, rather than passing it through + if config is not None: + config = to_unicode(config, "ascii", "config/hash") + config = self._unwrap_hash(config) + return self._wrap_hash(self.wrapped.genhash(secret, config, **kwds)) + + @deprecated_method(deprecated="1.7", removed="2.0", replacement=".hash()") + def encrypt(self, secret, **kwds): + return self.hash(secret, **kwds) + + def hash(self, secret, **kwds): + return self._wrap_hash(self.wrapped.hash(secret, **kwds)) + + def verify(self, secret, hash, **kwds): + hash = to_unicode(hash, "ascii", "hash") + hash = self._unwrap_hash(hash) + return self.wrapped.verify(secret, hash, **kwds) + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/utils/md4.py b/ansible/lib/python3.11/site-packages/passlib/utils/md4.py new file mode 100644 index 000000000..ef3f7a5d3 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/utils/md4.py @@ -0,0 +1,29 @@ +""" +passlib.utils.md4 - DEPRECATED MODULE, WILL BE REMOVED IN 2.0 + +MD4 should now be looked up through ``passlib.crypto.digest.lookup_hash("md4").const``, +which provides unified handling stdlib implementation (if present). +""" +#============================================================================= +# issue deprecation warning for module +#============================================================================= +from warnings import warn +warn("the module 'passlib.utils.md4' is deprecated as of Passlib 1.7, " + "and will be removed in Passlib 2.0, please use " + "'lookup_hash(\"md4\").const()' from 'passlib.crypto' instead", + DeprecationWarning) + +#============================================================================= +# backwards compat exports +#============================================================================= +__all__ = ["md4"] + +# this should use hashlib version if available, +# and fall back to builtin version. +from passlib.crypto.digest import lookup_hash +md4 = lookup_hash("md4").const +del lookup_hash + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/utils/pbkdf2.py b/ansible/lib/python3.11/site-packages/passlib/utils/pbkdf2.py new file mode 100644 index 000000000..273143b38 --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/utils/pbkdf2.py @@ -0,0 +1,193 @@ +"""passlib.pbkdf2 - PBKDF2 support + +this module is getting increasingly poorly named. +maybe rename to "kdf" since it's getting more key derivation functions added. +""" +#============================================================================= +# imports +#============================================================================= +from __future__ import division +# core +import logging; log = logging.getLogger(__name__) +# site +# pkg +from passlib.exc import ExpectedTypeError +from passlib.utils.decor import deprecated_function +from passlib.utils.compat import native_string_types +from passlib.crypto.digest import norm_hash_name, lookup_hash, pbkdf1 as _pbkdf1, pbkdf2_hmac, compile_hmac +# local +__all__ = [ + # hash utils + "norm_hash_name", + + # prf utils + "get_prf", + + # kdfs + "pbkdf1", + "pbkdf2", +] + +#============================================================================= +# issue deprecation warning for module +#============================================================================= +from warnings import warn + +warn("the module 'passlib.utils.pbkdf2' is deprecated as of Passlib 1.7, " + "and will be removed in Passlib 2.0, please use 'passlib.crypto' instead", + DeprecationWarning) + +#============================================================================= +# hash helpers +#============================================================================= + +norm_hash_name = deprecated_function(deprecated="1.7", removed="1.8", func_module=__name__, + replacement="passlib.crypto.digest.norm_hash_name")(norm_hash_name) + +#============================================================================= +# prf lookup +#============================================================================= + +#: cache mapping prf name/func -> (func, digest_size) +_prf_cache = {} + +#: list of accepted prefixes +_HMAC_PREFIXES = ("hmac_", "hmac-") + +def get_prf(name): + """Lookup pseudo-random family (PRF) by name. + + :arg name: + This must be the name of a recognized prf. + Currently this only recognizes names with the format + :samp:`hmac-{digest}`, where :samp:`{digest}` + is the name of a hash function such as + ``md5``, ``sha256``, etc. + + todo: restore text about callables. + + :raises ValueError: if the name is not known + :raises TypeError: if the name is not a callable or string + + :returns: + a tuple of :samp:`({prf_func}, {digest_size})`, where: + + * :samp:`{prf_func}` is a function implementing + the specified PRF, and has the signature + ``prf_func(secret, message) -> digest``. + + * :samp:`{digest_size}` is an integer indicating + the number of bytes the function returns. + + Usage example:: + + >>> from passlib.utils.pbkdf2 import get_prf + >>> hmac_sha256, dsize = get_prf("hmac-sha256") + >>> hmac_sha256 + + >>> dsize + 32 + >>> digest = hmac_sha256('password', 'message') + + .. deprecated:: 1.7 + + This function is deprecated, and will be removed in Passlib 2.0. + This only related replacement is :func:`passlib.crypto.digest.compile_hmac`. + """ + global _prf_cache + if name in _prf_cache: + return _prf_cache[name] + if isinstance(name, native_string_types): + if not name.startswith(_HMAC_PREFIXES): + raise ValueError("unknown prf algorithm: %r" % (name,)) + digest = lookup_hash(name[5:]).name + def hmac(key, msg): + return compile_hmac(digest, key)(msg) + record = (hmac, hmac.digest_info.digest_size) + elif callable(name): + # assume it's a callable, use it directly + digest_size = len(name(b'x', b'y')) + record = (name, digest_size) + else: + raise ExpectedTypeError(name, "str or callable", "prf name") + _prf_cache[name] = record + return record + +#============================================================================= +# pbkdf1 support +#============================================================================= +def pbkdf1(secret, salt, rounds, keylen=None, hash="sha1"): + """pkcs#5 password-based key derivation v1.5 + + :arg secret: passphrase to use to generate key + :arg salt: salt string to use when generating key + :param rounds: number of rounds to use to generate key + :arg keylen: number of bytes to generate (if ``None``, uses digest's native size) + :param hash: + hash function to use. must be name of a hash recognized by hashlib. + + :returns: + raw bytes of generated key + + .. note:: + + This algorithm has been deprecated, new code should use PBKDF2. + Among other limitations, ``keylen`` cannot be larger + than the digest size of the specified hash. + + .. deprecated:: 1.7 + + This has been relocated to :func:`passlib.crypto.digest.pbkdf1`, + and this version will be removed in Passlib 2.0. + *Note the call signature has changed.* + """ + return _pbkdf1(hash, secret, salt, rounds, keylen) + +#============================================================================= +# pbkdf2 +#============================================================================= +def pbkdf2(secret, salt, rounds, keylen=None, prf="hmac-sha1"): + """pkcs#5 password-based key derivation v2.0 + + :arg secret: + passphrase to use to generate key + + :arg salt: + salt string to use when generating key + + :param rounds: + number of rounds to use to generate key + + :arg keylen: + number of bytes to generate. + if set to ``None``, will use digest size of selected prf. + + :param prf: + psuedo-random family to use for key strengthening. + this must be a string starting with ``"hmac-"``, followed by the name of a known digest. + this defaults to ``"hmac-sha1"`` (the only prf explicitly listed in + the PBKDF2 specification) + + .. rst-class:: warning + + .. versionchanged 1.7: + + This argument no longer supports arbitrary PRF callables -- + These were rarely / never used, and created too many unwanted codepaths. + + :returns: + raw bytes of generated key + + .. deprecated:: 1.7 + + This has been deprecated in favor of :func:`passlib.crypto.digest.pbkdf2_hmac`, + and will be removed in Passlib 2.0. *Note the call signature has changed.* + """ + if callable(prf) or (isinstance(prf, native_string_types) and not prf.startswith(_HMAC_PREFIXES)): + raise NotImplementedError("non-HMAC prfs are not supported as of Passlib 1.7") + digest = prf[5:] + return pbkdf2_hmac(digest, secret, salt, rounds, keylen) + +#============================================================================= +# eof +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/passlib/win32.py b/ansible/lib/python3.11/site-packages/passlib/win32.py new file mode 100644 index 000000000..223dd6c8e --- /dev/null +++ b/ansible/lib/python3.11/site-packages/passlib/win32.py @@ -0,0 +1,68 @@ +"""passlib.win32 - MS Windows support - DEPRECATED, WILL BE REMOVED IN 1.8 + +the LMHASH and NTHASH algorithms are used in various windows related contexts, +but generally not in a manner compatible with how passlib is structured. + +in particular, they have no identifying marks, both being +32 bytes of binary data. thus, they can't be easily identified +in a context with other hashes, so a CryptHandler hasn't been defined for them. + +this module provided two functions to aid in any use-cases which exist. + +.. warning:: + + these functions should not be used for new code unless an existing + system requires them, they are both known broken, + and are beyond insecure on their own. + +.. autofunction:: raw_lmhash +.. autofunction:: raw_nthash + +See also :mod:`passlib.hash.nthash`. +""" + +from warnings import warn +warn("the 'passlib.win32' module is deprecated, and will be removed in " + "passlib 1.8; please use the 'passlib.hash.nthash' and " + "'passlib.hash.lmhash' classes instead.", + DeprecationWarning) + +#============================================================================= +# imports +#============================================================================= +# core +from binascii import hexlify +# site +# pkg +from passlib.utils.compat import unicode +from passlib.crypto.des import des_encrypt_block +from passlib.hash import nthash +# local +__all__ = [ + "nthash", + "raw_lmhash", + "raw_nthash", +] +#============================================================================= +# helpers +#============================================================================= +LM_MAGIC = b"KGS!@#$%" + +raw_nthash = nthash.raw_nthash + +def raw_lmhash(secret, encoding="ascii", hex=False): + """encode password using des-based LMHASH algorithm; returns string of raw bytes, or unicode hex""" + # NOTE: various references say LMHASH uses the OEM codepage of the host + # for its encoding. until a clear reference is found, + # as well as a path for getting the encoding, + # letting this default to "ascii" to prevent incorrect hashes + # from being made w/o user explicitly choosing an encoding. + if isinstance(secret, unicode): + secret = secret.encode(encoding) + ns = secret.upper()[:14] + b"\x00" * (14-len(secret)) + out = des_encrypt_block(ns[:7], LM_MAGIC) + des_encrypt_block(ns[7:], LM_MAGIC) + return hexlify(out).decode("ascii") if hex else out + +#============================================================================= +# eoc +#============================================================================= diff --git a/ansible/lib/python3.11/site-packages/pip/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/__pycache__/__init__.cpython-311.pyc index ef7e2384b8b01d99abe90178a1dc36e18ea69036..a9e8cabc0b898a9004fba088ce3401e7071f1a2d 100644 GIT binary patch delta 20 acmZo*YhdGE&dbZi00g@Yw`}D8$^-x|)CD#G delta 20 acmZo*YhdGE&dbZi00i$3ZQjWJl?ebdp#}N? diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/__pycache__/__init__.cpython-311.pyc index 79efdf779de0d965e6fadb10bc161f9b333f27da..028b1ed956c4294dd1957ede0bcb57fab0b97e42 100644 GIT binary patch delta 20 acmdnazMY+WIWI340}$*s+_I5-4Kn~Xkp*4= delta 20 acmdnazMY+WIWI340}#AFw0R@<8fE}HUIron diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/__pycache__/build_env.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/__pycache__/build_env.cpython-311.pyc index 0ba52fcc6a3d82011cfa7aeed92ba7e8c1468f36..6364b667a6d5b43d487440787ef8672761aa743b 100644 GIT binary patch delta 20 acmX?Jd%Tu=IWI340}$*s+_I5-j~xI> delta 20 acmX?Jd%Tu=IWI340}#AFw0R@<9y~ delta 20 acmaD+^rDD+IWI340}#AFw0R?UtR(08ShRwg3PC diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/__pycache__/exceptions.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/__pycache__/exceptions.cpython-311.pyc index 3f98eed90651481dd79fefe815b5612eca2efbe8..34d7d7dab61e9381982baae684131e71b33e3eff 100644 GIT binary patch delta 22 ccmcbzn(4}FChq0Dyj%=Gu-kCUM(&eS0at_vlK=n! delta 22 dcmcbzn(4}FChq0Dyj%=G@cz)|joc@v0svf52v-0A diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/__pycache__/pyproject.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/__pycache__/pyproject.cpython-311.pyc index ea0dd69133db1bb481b8a1e65ef86a645fa1e12c..42a77c78d57bf3c0dbefab64a0296a29c7446437 100644 GIT binary patch delta 20 acmeCz?$_pC&dbZi00g@Yw`}BY5d{D>a|J*E delta 20 acmeCz?$_pC&dbZi00i$3ZQjV;A_@RHKn4T= diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/__pycache__/self_outdated_check.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/__pycache__/self_outdated_check.cpython-311.pyc index 822d5bec629a0d95981361c3d61dee4d207d418b..0b783ecc93bcff71e645d730f882f0955723c980 100644 GIT binary patch delta 20 acmdlPu`_~uIWI340}$*s+_I5dPX_=&ZUwIZ delta 20 acmdlPu`_~uIWI340}#AFw0R@9o(=#;I|g$A diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/__pycache__/wheel_builder.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/__pycache__/wheel_builder.cpython-311.pyc index 04441f530e8e6cad6604722939d2df4a3c332963..066cfb983e8cefa8e570adfc02c3a0af23018d00 100644 GIT binary patch delta 20 acmexf^Sy?9IWI340}$*s+_I57#|{8cKnBAA delta 20 acmexf^Sy?9IWI340}#AFw0R?UjvWA14F`t+ diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/cli/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/cli/__pycache__/__init__.cpython-311.pyc index cdca623a7c1dcb4bfbdbac017f5bb53217b310bd..61aab3a1395206d95e1827059456c28ea2700014 100644 GIT binary patch delta 20 acmbQoG>?gUIWI340}$*s+_I5dm=ORl^8|+g delta 20 acmbQoG>?gUIWI340}#AFw0R@9Fe3mpzy(VH diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/cli/__pycache__/autocompletion.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/cli/__pycache__/autocompletion.cpython-311.pyc index 338d316498dc62bc80910ae9f05456b7d16699a8..6aaacf19f88baf691e2fb7dfbd933e2fd8a63459 100644 GIT binary patch delta 20 acmccPcgK%=IWI340}$*s+_I57P#pk8c?JIf delta 20 acmccPcgK%=IWI340}#AFw0R?UpgI6bMh3$G diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/cli/__pycache__/base_command.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/cli/__pycache__/base_command.cpython-311.pyc index 76bb95543c48292aa54d6d62ad99fc03b3714849..b2829afe6d7d4db3149fd6d37879c3557f2d4eff 100644 GIT binary patch delta 20 acmX>Ub|{Q{IWI340}$*s+_I6|TpIvFjRnO3 delta 20 acmX>Ub|{Q{IWI340}#AFw0R@9xi$btS_X*# diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/cli/__pycache__/cmdoptions.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/cli/__pycache__/cmdoptions.cpython-311.pyc index e043d613b022483d9b0f014a727b7d940926597f..c42c16c4d4bf4a1a000c0fa1e08caf03751d8f51 100644 GIT binary patch delta 22 ccmX@x$aJ=miF-LOFBbz4>^9u8k^4{s08iWp6#xJL delta 22 ccmX@x$aJ=miF-LOFBbz4yg#&gBln>O092_5*#H0l diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/cli/__pycache__/command_context.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/cli/__pycache__/command_context.cpython-311.pyc index 123b01b9ed225a4ddc8ab3d80efa18faad2d0d64..5711ef2df1b42e8c2aee38b55cbc78ace657ac20 100644 GIT binary patch delta 20 acmdlZut$Jjg;w delta 20 acmbQFJxQB;IWI340}#AFw0R?UyC?uUxCRXX diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/cli/__pycache__/parser.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/cli/__pycache__/parser.cpython-311.pyc index 9b9471edc1681a772b3107c3239321f2811f157b..023355f12599951041d9dfe57733c05ffb31567a 100644 GIT binary patch delta 22 ccmez0!uY?1k$X8WFBbz4>^9u8k-NkR09RB7@c;k- delta 22 ccmez0!uY?1k$X8WFBbz4yg#&gBX@}t09+vlwEzGB diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/cli/__pycache__/progress_bars.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/cli/__pycache__/progress_bars.cpython-311.pyc index 29a2277fe8bc85124ae772670ccc736b9f7f7f0e..0e4df663a2ebbb5e9ac77207ceca3a41d69e9c64 100644 GIT binary patch delta 20 acmaDP@koMuIWI340}$*s+_I57oCg3twgrj+ delta 20 acmaDP@koMuIWI340}#AFw0R?UI1d0rg9c6j diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/cli/__pycache__/req_command.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/cli/__pycache__/req_command.cpython-311.pyc index be712fc66aaebafadcd043e1b15d207eabe8a599..69224f6d8e3612acb4d040b585467119213ece1d 100644 GIT binary patch delta 22 ccmZ2JmvQ-AM(*Xjyj%=Gu-kCUM(#O&08M8GrvLx| delta 22 ccmZ2JmvQ-AM(*Xjyj%=G@cz)|jofqm08%suYXATM diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/cli/__pycache__/spinners.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/cli/__pycache__/spinners.cpython-311.pyc index 08eb50da625b2661d005577a6f98006756576c9d..7d4febd9b74963a4355cf7e76bf076ab3e05aee0 100644 GIT binary patch delta 20 acmZp6ZFc2e&dbZi00g@Yw`}CDPyzrqNCjO0 delta 20 acmZp6ZFc2e&dbZi00i$3ZQjUTp#%Ut6$T*y diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/cli/__pycache__/status_codes.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/cli/__pycache__/status_codes.cpython-311.pyc index 7194a4aa42739217e4dd00d9601ea21c62c6f2b7..371478ee07d6b30741a26df709562e8358af1886 100644 GIT binary patch delta 20 acmeyy^o@yoIWI340}$*s+_I57n-Ks$vjvX; delta 20 acmeyy^o@yoIWI340}#AFw0R?UHX{H-fCf_l diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/commands/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/commands/__pycache__/__init__.cpython-311.pyc index 02c8bd87360e311ccec522f698c18a998dc761ee..2ce1b37dc4365fcddea78b4c9ceaddf2eb8a81ca 100644 GIT binary patch delta 20 acmaE+^h}9+IWI340}$*s+_I57S`Yw0!v(JZ delta 20 acmaE+^h}9+IWI340}#AFw0R?Uv>*UQkOp%A diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/commands/__pycache__/install.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/commands/__pycache__/install.cpython-311.pyc index 5b71cc0e7aeffde71b92f93ffcc13aeb01d08ecc..b527c06c076ef590dd25d4317992870c94c013dc 100644 GIT binary patch delta 22 ccmZ2Gg=y^+Chq0Dyj%=Gu-kCUMsB4p08DEJWdHyG delta 22 ccmZ2Gg=y^+Chq0Dyj%=G@cz)|joeCI08uyxDF6Tf diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/distributions/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/distributions/__pycache__/__init__.cpython-311.pyc index 4a319905122807f090e4c87088f9090e81406be7..b2667aa6fead3b7a71c845ea9c0216b280cb2a97 100644 GIT binary patch delta 20 acmeC-=;7dA&dbZi00g@Yw`}BQWB~v!{sdqE delta 20 acmeC-=;7dA&dbZi00i$3ZQjVu$N~T}%LOC= diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/distributions/__pycache__/base.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/distributions/__pycache__/base.cpython-311.pyc index a3adafc80f4008d84ed987db791b6c84170a80e8..44257fd1785985436599211fd25d1f3485f5be21 100644 GIT binary patch delta 20 acmaDM^g@VxIWI340}$*s+_I57mJEz*F&dbZi00g@Yw`}D8#|i*37X@Gd delta 20 acmeC=>Ez*F&dbZi00i$3ZQjWJj}-tlxBvhE delta 22 dcmdmWlWEsYChq0Dyj%=G@cz)|jojkH#v&dbZi00g@Yw`}BQ)&>ANZ3QU+ delta 20 acmeAR>kH#v&dbZi00i$3ZQjVutPKD^ItA?j diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/locations/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/locations/__pycache__/__init__.cpython-311.pyc index 85a13ea828448c9a7622e9036d3839a07e46ece3..c2d7625922003582cc37d528e1a2675b5624ce00 100644 GIT binary patch delta 22 ccmZqbV{GbU$o<_707|R}RsaA1 diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/locations/__pycache__/_sysconfig.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/locations/__pycache__/_sysconfig.cpython-311.pyc index 9035e69f1ebed1f586c7e90de939983327b2d41a..25e087b090fbc36a69e6423ee45f8b7c5c7f1def 100644 GIT binary patch delta 20 acmdnwy2+J$IWI340}$*s+_I5-xe@?Apatpx delta 20 acmdnwy2+J$IWI340}#AFw0R@NetD&dbZi00g@Yw=CqA1OPAg1hxPG delta 19 ZcmeA+>NetD&dbZi00i$3ZC=PN2>>(y1$qDg diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/metadata/__pycache__/_json.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/metadata/__pycache__/_json.cpython-311.pyc index f8b4daa63e0769c83fbd6846f8db646c11d68ffc..1e07da3fe8daa1482b23a5f0c2e4c78e053d1c78 100644 GIT binary patch delta 20 acmaDW{Z^WLIWI340}$*s+_I7T5ibBi+XgZK delta 20 acmaDW{Z^WLIWI340}#AFw0R@^9u8k^8(O08#Y@{{R30 delta 22 ccmccB%y_Grk$X8WFBbz4yg#&gBlmen09L{W!vFvP diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/metadata/importlib/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/metadata/importlib/__pycache__/__init__.cpython-311.pyc index bb24fa07ed879700c23fffe0af68047e9b6ca2c4..025b33eaac39b8fd11510368afcafcd746d45edd 100644 GIT binary patch delta 20 acmaFL^puHvIWI340}$*s+_I57iV*-j3k6&N delta 20 acmaFL^puHvIWI340}#AFw0R?U6e9pY*ajc~ diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/metadata/importlib/__pycache__/_compat.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/metadata/importlib/__pycache__/_compat.cpython-311.pyc index 7d7f97fc83ad450a5225a47a869de8933f414040..9e33fc17e4d7f26f3b5c59d74a466c4d3ebd0a9c 100644 GIT binary patch delta 20 acmaDO{YIL5IWI340}$*s+_I7TAuj+yvj#5! delta 20 acmaDO{YIL5IWI340}#AFw0R@jq>1 diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/models/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/models/__pycache__/__init__.cpython-311.pyc index d3927d9b099132117eac94f647b3348dd6cc7b56..584d1bda06bd4afd6cb0c777468d226bb44575f8 100644 GIT binary patch delta 20 acmbQlG>M6OIWI340}$*s+_I6Ioe=;p69i)b delta 20 acmbQlG>M6OIWI340}#AFw0R>pJ0k!y-~}fD diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/models/__pycache__/candidate.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/models/__pycache__/candidate.cpython-311.pyc index bdea2c0e625f1dcd0c63ccfc180baef2a2b3dae1..4095160881ced402b7d5e13db9fd35941f854a48 100644 GIT binary patch delta 20 acmZ20uvUP3IWI340}$*s+_I5di30#NN(9#c delta 20 acmZ20uvUP3IWI340}#AFw0R@95(fY}7X_OD diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/models/__pycache__/direct_url.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/models/__pycache__/direct_url.cpython-311.pyc index 082f938e35a75c8f6b635052160236544a032243..5e600c2cce2d4a74886157897b266e0cc90be895 100644 GIT binary patch delta 20 acmaD5{~(@wIWI340}$*s+_I7Tsy+Zog$A_% delta 20 acmaD5{~(@wIWI340}#AFw0R@n8IWI340}$*s+_I5diwgiZ90d6Q delta 20 acmdlgvQ>n8IWI340}#AFw0R@978d|J=>@$2 diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/models/__pycache__/link.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/models/__pycache__/link.cpython-311.pyc index 07363622289db453c74a5ae8adf4a9401e3739d5..0d4b3283000844d6ef7cb9e4e5d066f4437b265e 100644 GIT binary patch delta 22 ccmX?mj`8d{M(*Xjyj%=Gu-kCUMsBBc09siG6aWAK delta 22 ccmX?mj`8d{M(*Xjyj%=G@cz)|joeP@0AD5t*Z=?k diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/models/__pycache__/scheme.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/models/__pycache__/scheme.cpython-311.pyc index 5077228e348c8798401ab2c528f06e94df9a3b2f..eddb5152a52dec7438874746fe78a3a1b1023411 100644 GIT binary patch delta 20 acmey!`H_=*IWI340}$*s+_I7T84CbE>IM-2 delta 20 acmey!`H_=*IWI340}#AFw0R@(IWI340}$*s+_I6oTL=I(umwf{ delta 20 acmbQHI!%>(IWI340}#AFw0R?Uw-5k2eFh2u diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/models/__pycache__/wheel.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/models/__pycache__/wheel.cpython-311.pyc index f3f69bf75907efcd88a19baff3d384d75fa65a1f..bd82d0917f187149a639fac9185b57369d7cd965 100644 GIT binary patch delta 20 acmbPgG}VZEIWI340}$*s+_I6IOA-J!V+7;? delta 20 acmbPgG}VZEIWI340}#AFw0R>pmm~l?Fa@Xp diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/network/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/network/__pycache__/__init__.cpython-311.pyc index 52e6018e678e65fea84395e2534fd65143c2beae..29b1983d4bdc12d62bb8be43940a195b15e52168 100644 GIT binary patch delta 19 ZcmZo;YGdMF&dbZi00g@Yw@l>z4FD;W1s(tZ delta 19 ZcmZo;YGdMF&dbZi00i$3ZJx;e8vrex1>yhz diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/network/__pycache__/auth.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/network/__pycache__/auth.cpython-311.pyc index abf60f0fcd675f5b7bd340913ed38dcbafe350a6..29f8d1bb1247e562ecb9aef91542937b37f88e11 100644 GIT binary patch delta 22 ccmex0h4I%EM(*Xjyj%=Gu-kCUM(%ts09f@03IG5A delta 22 ccmex0h4I%EM(*Xjyj%=G@cz)|jokTO0A0cd&Hw-a diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/network/__pycache__/cache.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/network/__pycache__/cache.cpython-311.pyc index 864ab3b3175290f2924cd1b83a31d05c5dbbcc25..365acff92fac76fbcaafe8051a60305df8fdd886 100644 GIT binary patch delta 20 acmX@8aZrPMIWI340}$*s+_I6|OauTs)&)-h delta 20 acmX@8aZrPMIWI340}#AFw0R@9nFs(tqXrWI diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/network/__pycache__/download.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/network/__pycache__/download.cpython-311.pyc index 8fcac5f3494ade1c5474c074d0c73cd8b0f3e8a1..be8368d14f731ebf4b461a636e4e9e3240411170 100644 GIT binary patch delta 20 acmaFq_0o%bIWI340}$*s+_I57P89$~?*<$I delta 20 acmaFq_0o%bIWI340}#AFw0R?UoGJiIyawO^ diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/network/__pycache__/lazy_wheel.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/network/__pycache__/lazy_wheel.cpython-311.pyc index d5462a2939d96d021e5e985ac446ef0d913e09a8..0ff8c9ae4f9957b600739bd01975edb2b57fa70e 100644 GIT binary patch delta 20 acmcbgdOwwWIWI340}$*s+_I7TiV*-xI0nH0 delta 20 acmcbgdOwwWIWI340}#AFw0R@<6(ayo1qX!y diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/network/__pycache__/session.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/network/__pycache__/session.cpython-311.pyc index a15a064fc4c145e0f6b961035aef6ac04ffe47d9..beb8edf04962d616fbe42f6bd3a46e5a7393e850 100644 GIT binary patch delta 22 ccmZ3rjB(X6M(*Xjyj%=Gu-kCUMsE3F08UH>M*si- delta 22 ccmZ3rjB(X6M(*Xjyj%=G@cz)|jok9V08<$U3jhEB diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/network/__pycache__/utils.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/network/__pycache__/utils.cpython-311.pyc index 9c52709e9798b2010e0faa65a14a84bcfaf6406e..fa0aa28a4714350f548c6cc50120b2aa0b7faacb 100644 GIT binary patch delta 20 acmaDY^je5}IWI340}$*s+_I57ffE2fLj{%q delta 20 acmaDY^je5}IWI340}#AFw0R?U0w(}O5C&QR diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/operations/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/operations/__pycache__/__init__.cpython-311.pyc index 7dd3b02241a407e8588b83557e0f341201f34a58..4e223376cf84440f378665da467cbe4d43a08ab1 100644 GIT binary patch delta 19 ZcmX@Xc!H68IWI340}$*s+%l1SF90%p1!4dI delta 19 ZcmX@Xc!H68IWI340}#AFw0R=;UH~?Q1||Ri diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/operations/__pycache__/check.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/operations/__pycache__/check.cpython-311.pyc index 9a10f34e7e25c50760d182002de3cbec4a8281c8..be218c4287ccb699518eade3a991c80b37a7778d 100644 GIT binary patch delta 20 acmaE9{L+|vIWI340}$*s+_I7To+JQ7rv_pG delta 20 acmaE9{L+|vIWI340}#AFw0R@^9u8k()6c08efPQ2+n{ delta 22 ccmeA<$Jld@k$X8WFBbz4yg#&gBR69@08~2%6#xJL diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/operations/build/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/operations/build/__pycache__/__init__.cpython-311.pyc index 6ff10486f5eadc376e301783b272157ff86ef796..9fd8e16970069c1a007e2d103aacc4fa46f04657 100644 GIT binary patch delta 19 ZcmX@dc#e^KIWI340}$*s+%l2-FaR@J1#|!a delta 19 ZcmX@dc#e^KIWI340}#AFw0R=;VE{K{1~>o! diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/operations/build/__pycache__/build_tracker.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/operations/build/__pycache__/build_tracker.cpython-311.pyc index c794cf459f4f5aab94ca27e8342be1b481936036..8747653c7ede92df2e31c88f598a89c4786e6254 100644 GIT binary patch delta 20 acmX?Tf6$(LIWI340}$*s+_I5-n>+wO9R>&h delta 20 acmX?Tf6$(LIWI340}#AFw0R@ITdJ diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/operations/build/__pycache__/metadata.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/operations/build/__pycache__/metadata.cpython-311.pyc index d8e051a1e2b1667b3e93e708374f51aa8fa01b30..cd09447b767c7e96a3f43338ea1ecc88ae170b9f 100644 GIT binary patch delta 20 acmaDV_*9U4IWI340}$*s+_I7T76$-7E(QSr delta 20 acmaDV_*9U4IWI340}#AFw0R@J;K$&dbZi00g@Yw`}D8#{mE_@&#`I delta 20 acmeAa>J;K$&dbZi00i$3ZQjWJj{^WUzXme^ diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/operations/build/__pycache__/metadata_legacy.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/operations/build/__pycache__/metadata_legacy.cpython-311.pyc index 37a2fcafe7bb2608faab00f5084e4f6e68b52e24..5c4d745c72b74dae3e26694787058d0575d477b3 100644 GIT binary patch delta 20 acmZpaZItC+&dbZi00g@Yw`}At;{yOOLg45e2sZ diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/operations/build/__pycache__/wheel.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/operations/build/__pycache__/wheel.cpython-311.pyc index e17b1f41b413b6a9159d4736d85ed389f3662e7f..863f3a9362b2ad5772c1e7dd61886ab8355ca3cf 100644 GIT binary patch delta 20 acmbQvKb@a@IWI340}$*s+_I6ohaCViwFMFY delta 20 acmbQvKb@a@IWI340}#AFw0R?U4?6%if(6z9 diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/operations/build/__pycache__/wheel_editable.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/operations/build/__pycache__/wheel_editable.cpython-311.pyc index cc31962f64b7ae0fda99d5a460bc8d74cb0d3950..9a6c08f0a833d0e5ac373f2a920c99c2deec7f87 100644 GIT binary patch delta 20 acmcaCbXkadIWI340}$*s+_I6|gA)Kdg9T0i delta 20 acmcaCbXkadIWI340}#AFw0R@92PXhPPzDkJ diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/operations/build/__pycache__/wheel_legacy.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/operations/build/__pycache__/wheel_legacy.cpython-311.pyc index b453d298a7c22ebd7dc21183c037a981f7cd40c6..3984b4782830939944a8f962da3d1c2d874e54fe 100644 GIT binary patch delta 20 acmeBE?o;Mo&dbZi00g@Yw`}BY76bq^Cj}`0 delta 20 acmeBE?o;Mo&dbZi00i$3ZQjV;EC>KN^abqz diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/operations/install/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/operations/install/__pycache__/__init__.cpython-311.pyc index 5988ca3ff29a873c7a39c3f96acd2fd5a8b3145a..321ba98f1a34f56acc567865400f4366f2fcbac5 100644 GIT binary patch delta 20 acmbQlG>M6OIWI340}$*s+_I6Ioe=;p69i)b delta 20 acmbQlG>M6OIWI340}#AFw0R>pJ0k!y-~}fD diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/operations/install/__pycache__/editable_legacy.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/operations/install/__pycache__/editable_legacy.cpython-311.pyc index 764669c69abf6daae4d8140a8748e966633066f4..5b51be08aecc73cee2c1d20dec0027b9aad6fdd7 100644 GIT binary patch delta 20 acmca7cu$afIWI340}$*s+_I7TG6w)Y76t48 delta 20 acmca7cu$afIWI340}#AFw0R@;M1& delta 22 ccmdn9gK5tWChq0Dyj%=G@cz)|jogNF09i{1umAu6 diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/req/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/req/__pycache__/__init__.cpython-311.pyc index d52c350399a5bea53bb46886e0458014b0238426..c26fcdc0125e9a27c7de736b05356e42e802eaf4 100644 GIT binary patch delta 20 acmcbobWe$UIWI340}$*s+_I57SP%d|g9VoW delta 20 acmcbobWe$UIWI340}#AFw0R?Uupj_LPzGB7 diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/req/__pycache__/constructors.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/req/__pycache__/constructors.cpython-311.pyc index e76803f1f3b967df46a2ce09299cc72a0ba29090..be1bf30256dac771b5d89cc3d99de0e5a984dd9e 100644 GIT binary patch delta 22 ccmaF3kn!O{M(*Xjyj%=Gu-kCUM(%3?09V5YUjP6A delta 22 ccmaF3kn!O{M(*Xjyj%=G@cz)|jojA)09=p=BLDyZ diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/req/__pycache__/req_file.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/req/__pycache__/req_file.cpython-311.pyc index 55b6e571964324d86296fcfb137e635dc95fc8e9..5234fbb0e3a0a0185a011957b60a8d22ebc77b57 100644 GIT binary patch delta 22 ccmZ3qo^jE7M(*Xjyj%=Gu-kCUM(*k108dv2#Q*>R delta 22 ccmZ3qo^jE7M(*Xjyj%=G@cz)|joj120aPRhi2wiq diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/req/__pycache__/req_install.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/req/__pycache__/req_install.cpython-311.pyc index 3453fa838b5eedb67058096cb23259c115397ff5..8033df0af0c934d46863cc3cfed17bea4bd0c535 100644 GIT binary patch delta 22 ccmbQWn`zc=Chq0Dyj%=Gu-kCUM(+N(08l;$4FCWD delta 22 ccmbQWn`zc=Chq0Dyj%=G@cz)|jokfn0aXhJ(EtDd diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/req/__pycache__/req_set.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/req/__pycache__/req_set.cpython-311.pyc index 49dc712da799b907fa767c0e9faa1e765b65f520..ef95689febe77d17beae09ef8c8275a918f28d47 100644 GIT binary patch delta 20 acmeyQ_eqa?IWI340}$*s+_I57T^s;JSq1R` delta 20 acmeyQ_eqa?IWI340}#AFw0R?Ux;OwyCI+^9u8k-K&R07*3lmH+?% delta 22 ccmeBJ$kefriF-LOFBbz4yg#&gBX{it08Ro2S^xk5 diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/resolution/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/resolution/__pycache__/__init__.cpython-311.pyc index c7c14d2ad6a27b906244ccace6b4f7e1071d0e2e..5ab84e02482450c250273e2fcc34b3afd17d27e4 100644 GIT binary patch delta 19 ZcmX@Xc!H68IWI340}$*s+%l1SF90%p1!4dI delta 19 ZcmX@Xc!H68IWI340}#AFw0R=;UH~?Q1||Ri diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/resolution/__pycache__/base.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/resolution/__pycache__/base.cpython-311.pyc index dc52097dccba16da18f15ad948021721c2846080..39fc6aa0f18a0ccf6a9fd82202680bb7cb3565f7 100644 GIT binary patch delta 20 acmcc3b(@QOIWI340}$*s+_I57fE55cp9N9? delta 20 acmcc3b(@QOIWI340}#AFw0R?U04o4MYz7tp diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/resolution/resolvelib/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/resolution/resolvelib/__pycache__/__init__.cpython-311.pyc index 92e82b87c56bff3bee3a786417712616d2455c57..2314b95d47fe9eba8b75b5992251d1a6fe022b00 100644 GIT binary patch delta 19 Zcmcc2c$txVIWI340}$*s+%l2-1OPM>1%m(p delta 19 Zcmcc2c$txVIWI340}#AFw0R=;2>>`821ft@ diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/resolution/resolvelib/__pycache__/base.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/resolution/resolvelib/__pycache__/base.cpython-311.pyc index 94f9d2d442adbf292a26de169ee3ff1e7206f618..26cff79938aeb347fc60978613eb67c250b17349 100644 GIT binary patch delta 20 acmbQ?J;R%OIWI340}$*s+_I6oR}}y|YXz$S delta 20 acmbQ?J;R%OIWI340}#AFw0R?UuPOjQI0kP3 diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/resolution/resolvelib/__pycache__/candidates.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/resolution/resolvelib/__pycache__/candidates.cpython-311.pyc index 9a1d7d342f37cc815102329418257e376d3773c4..9b6768ee18c6f3cdac1fffa861186d793a7130af 100644 GIT binary patch delta 22 ccmZ4Vka5vNM(*Xjyj%=Gu-kCUM(*ha09OtNLI3~& delta 22 ccmZ4Vka5vNM(*Xjyj%=G@cz)|joi}<09)G#1^@s6 diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/resolution/resolvelib/__pycache__/factory.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/resolution/resolvelib/__pycache__/factory.cpython-311.pyc index 17bb2a8e04a89e745519d03a11d237280f576840..49376de0793624eb1baa1942a660b6b9f35f22a9 100644 GIT binary patch delta 22 ccmaF$lkwG0M(*Xjyj%=Gu-kCUM(+DH0A|_vO$G=IWI340}$*s+_I5dRR{n$oCOj9 delta 20 acmdm>vO$G=IWI340}#AFw0R@9st^D@X$95* diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/resolution/resolvelib/__pycache__/requirements.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/resolution/resolvelib/__pycache__/requirements.cpython-311.pyc index ad591e03097820f15aa10b7c58de23a50f9d49f1..af5299dabb5d2d082e42e5f49cd9a7d5a0c28418 100644 GIT binary patch delta 20 acmewu_A!imIWI340}$*s+_I57O&b77prvU&#mj)OB diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/__init__.cpython-311.pyc index d141135218c7eb601b4294b850b158dd7e6c04aa..6f620f8e8cba054e5d385a29207f87124566e511 100644 GIT binary patch delta 19 ZcmX@ic$krUIWI340}$*s+%l1S2LLg{1ycY3 delta 19 ZcmX@ic$krUIWI340}#AFw0R=;4gfXG1{VMT diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/_log.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/_log.cpython-311.pyc index 8736d0ce78c09784917b25d82cf857a0df5bdc10..fec2ca291cd722fa5d6b29c64fcf3220e5688d1e 100644 GIT binary patch delta 20 acmaFB|A3!+IWI340}$*s+_I7TDmwr^9R=+G delta 20 acmaFB|A3!+IWI340}#AFw0R@ISg@ diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/appdirs.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/appdirs.cpython-311.pyc index 2e184d7282daedfdab9b175713520cb72f49898b..5adceeb842050e8cfbb07317a71d4b7e69a57cac 100644 GIT binary patch delta 20 acmew*{7aa7IWI340}$*s+_I7TJtqJ{zXnAB delta 20 acmew*{7aa7IWI340}#AFw0R@+9vIR*1 delta 20 acmZ1|xlodOIWI340}#AFw0R@J#E#&dbZi00g@Yw`}BQ<^%vRy99p# delta 20 acmeAZ>J#E#&dbZi00i$3ZQjVu%n1NBhy_Cc diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/entrypoints.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/entrypoints.cpython-311.pyc index 36e4b991e548ea913f68e8dbc39e7732dbdf4671..a3c09896c71993970ac96149ba9d7f0f85882226 100644 GIT binary patch delta 20 acmbQBI6;wnIWI340}$*s+_I6oRR91qE(Iz8 delta 20 acmbQBI6;wnIWI340}#AFw0R?Us{jBv`vvX* diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/filesystem.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/filesystem.cpython-311.pyc index a233d0e9d22f90271cc40e4969f466c5eb29f6ee..f194f06b07827698edba5f694106b3aed5e2c44b 100644 GIT binary patch delta 20 acmZ4Ju+V{fIWI340}$*s+_I5dOaTBoLIo=T delta 20 acmZ4Ju+V{fIWI340}#AFw0R@9m;wMl4+ZZ4 diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/filetypes.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/filetypes.cpython-311.pyc index e178cb0b7ea625445cea1b9fec4013c14e2788af..d46c20b8350396ad3b311640a0c3699ca5e7285e 100644 GIT binary patch delta 20 acmbQwHJ^)nIWI340}$*s+_I5dgcSfXKLnlt delta 20 acmbQwHJ^)nIWI340}#AFw0R@92rB?L3~Z8?&dbZi00g@Yw`}BYQ~&@t9tCay delta 20 acmeBi>~Z8?&dbZi00i$3ZQjV;r~m*w>jp9a diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/inject_securetransport.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/inject_securetransport.cpython-311.pyc index 93713ccd3e165ad57e591cfe53d7dee6cd7d1451..21f369b7b9bf708cef7793068412f01a33e26f82 100644 GIT binary patch delta 20 acmdnUwULW^IWI340}$*s+_I5djTHbj4+PWz delta 20 acmdnUwULW^IWI340}#AFw0R@98Y=)f+y$5b diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/logging.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/logging.cpython-311.pyc index dd79c38ab9ebb8cfcff59505268e11e10e7d3eee..fb096fce0836f421f9192f366b321b2ff9a873ae 100644 GIT binary patch delta 20 acmcataj$}VIWI340}$*s+_I57*aiSg#s+Qx delta 20 acmcataj$}VIWI340}#AFw0R?UunhoElLs;Y diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/misc.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/misc.cpython-311.pyc index 4cd7c1ec5ba4ebdd4f683bf08fe7d213183d23de..5d24e5a5828d98901cc76cc4fabc7718c38eb71e 100644 GIT binary patch delta 22 ccmX@GjOoBKChq0Dyj%=Gu-kCUMsCx|08(QH%K!iX delta 22 ccmX@GjOoBKChq0Dyj%=G@cz)|johY_0aq{wj{pDw diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/models.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/models.cpython-311.pyc index a6d45e5a1981b6531d8da12ede451b58fdb1c284..3c421d5499d1b9c4d85e30fae0bd56d703adb0a5 100644 GIT binary patch delta 20 acmew^_Fas7IWI340}$*s+_I57hZ_JvwFSfg delta 20 acmew^_Fas7IWI340}#AFw0R?U4mSWsf(D2H diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/packaging.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/packaging.cpython-311.pyc index bc52de6bc797f60e7fc065fe346e3f1995a98052..3ea955d9d5470b7b6adc1f572972e15dbc77d53d 100644 GIT binary patch delta 20 acmew)`bm^~IWI340}$*s+_I7TITrvy$ObY1 delta 20 acmew)`bm^~IWI340}#AFw0R@?=IWI340}$*s+_I5-ni>E-90kh& delta 20 acmZ4JyU>?=IWI340}#AFw0R@! delta 20 acmbOcIU|yLIWI340}#AFw0R?UuMPl0HwKab diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/unpacking.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/unpacking.cpython-311.pyc index f68b48fd6bcc0d793d7f96797a0928398636627b..9a613c3a2dd3dbea341134904ea7cdb2fd948d8d 100644 GIT binary patch delta 20 acmcbeayx~4IWI340}$*s+_I57zz6_HbOtj3 delta 20 acmcbeayx~4IWI340}#AFw0R?UfDr&qK?e5# diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/urls.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/urls.cpython-311.pyc index eb193898a392030546391cc43f0cce63d738d50d..2c454f96e865c698e7fae545331daa08ab2136e3 100644 GIT binary patch delta 20 acmZn=Z4l*N&dbZi00g@Yw`}AtA*eFa^9u8k((t107?=C`v3p{ delta 22 ccmeBQ!Pviok$X8WFBbz4yg#&gBR5M308ZZqzW@LL diff --git a/ansible/lib/python3.11/site-packages/pip/_internal/vcs/__pycache__/mercurial.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_internal/vcs/__pycache__/mercurial.cpython-311.pyc index 1222dd2c194eb980343caaf4c2d77c5185e16370..5d7649780eb820a4d01158e6313353b89edc65bf 100644 GIT binary patch delta 20 acmezC{MVU#IWI340}$*s+_I7TlOh004+h2n delta 20 acmezC{MVU#IWI340}#AFw0R@uL diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/cachecontrol/__pycache__/controller.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/cachecontrol/__pycache__/controller.cpython-311.pyc index 9a78edaef9e0277dd7874804e93be0c3caff6656..d604223d4fa2db95bde73d229aba4b8173f277c5 100644 GIT binary patch delta 22 ccmdnnz__=8k$X8WFBbz4>^9u8k=w`t085|-ApigX delta 22 ccmdnnz__=8k$X8WFBbz4yg#&gBe#(Q08niP`>%h&dbZi00g@Yw`}CD6#xJ-?gbJ6 delta 20 acmeBB>`>%h&dbZi00i$3ZQjUTD*ymDy9L$& diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/cachecontrol/__pycache__/serialize.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/cachecontrol/__pycache__/serialize.cpython-311.pyc index c680b6e0659b81df9ee4265d863368679af0867b..054bcf8cb9c21d1676110ad1c2f508c8f99e35a4 100644 GIT binary patch delta 20 acmX@%c*2o;IWI340}$*s+_I5-uL1x;eg+}{ delta 20 acmX@%c*2o;IWI340}#AFw0R@9 diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/cachecontrol/caches/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/cachecontrol/caches/__pycache__/__init__.cpython-311.pyc index c783a1ba864249838fbe59e0521c180b755102d8..ea469ec446ef27ab9e2c7d009d4917adbbbac10b 100644 GIT binary patch delta 20 acmbQkJcpTkIWI340}$*s+_I5-A|n7X#{~lb delta 20 acmbQkJcpTkIWI340}#AFw0R@jfYH delta 20 acmcb}bdiaBIWI340}#AFw0R@98zTTcxCP_@ diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/certifi/__pycache__/core.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/certifi/__pycache__/core.cpython-311.pyc index d0a2f03ac3b76dac7a50c770ba31ea2359f64407..cdac31e7014092672d75aa087e4d957728fe67df 100644 GIT binary patch delta 20 acmeB??~&(T&dbZi00g@Yw`}BYnBCM(*Xjyj%=Gu-kCUMsAZV09fn>`2YX_ delta 22 ccmdmgg>nBCM(*Xjyj%=G@cz)|joc<#0A0BUy#N3J diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/chardet/__pycache__/big5prober.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/chardet/__pycache__/big5prober.cpython-311.pyc index 9f8cc22872ac86ad7a2651d3eeb810b00211df1a..99dee0d3ddd2e2f213cab43031c43c949ba27e08 100644 GIT binary patch delta 20 acmeC;?c(KL&dbZi00g@Yw`}CDX9EB*WCZ2_ delta 20 acmeC;?c(KL&dbZi00i$3ZQjUT&jtWBF$Jms diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/chardet/__pycache__/chardistribution.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/chardet/__pycache__/chardistribution.cpython-311.pyc index 1f4c5da8ef8e2ca17f22866a2086ad344016564b..5617c87fe730df4178d482190679715c8678d500 100644 GIT binary patch delta 20 acmZpQXo}!o&dbZi00g@Yw`}D8t_=V>)dmRw delta 20 acmZpQXo}!o&dbZi00i$3ZQjWJT^j&Dq6Wy9Sm3 diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/chardet/__pycache__/charsetprober.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/chardet/__pycache__/charsetprober.cpython-311.pyc index 98ec91e5ffc229a17e76aaab9b7240854952c278..0162c1b539c8d25b231326dc6eb361a8f2bdb24e 100644 GIT binary patch delta 20 acmZ3cy-b^XIWI340}$*s+_I5-wkQBO3k8J$ delta 20 acmZ3cy-b^XIWI340}#AFw0R@$?m%m4rY diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/chardet/__pycache__/euctwprober.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/chardet/__pycache__/euctwprober.cpython-311.pyc index 0abd7673cbb743c11f15b18b06b32d84b7f5ca9d..3353f96659efa1e94851da27495150a1f3e68d0c 100644 GIT binary patch delta 20 acmeC??dIiP&dbZi00g@Yw`}BYU;_X!j0EWb delta 20 acmeC??dIiP&dbZi00i$3ZQjV;zy<&{Sp}^C diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/chardet/__pycache__/gb2312freq.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/chardet/__pycache__/gb2312freq.cpython-311.pyc index 9012aeb22d899481eb52dd87c967ba97410c9d1f..bcad1873c4a3f866505b8f2b16c6f1c162bcd988 100644 GIT binary patch delta 22 ccmdlsm2ultM(*Xjyj%=Gu-kCUM()*K08X$5zyJUM delta 22 ccmdlsm2ultM(*Xjyj%=G@cz)|johof08@Pjga7~l diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/chardet/__pycache__/gb2312prober.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/chardet/__pycache__/gb2312prober.cpython-311.pyc index 1e157de279a335f3b3d1880ab92aa4cbd38827e4..b151fc348f08c6323a975cf2eb67dd50f691c086 100644 GIT binary patch delta 20 acmbQmJByclIWI340}$*s+_I6opA7&q^9u8k-H%b081+ddjJ3c delta 22 ccmeBP%hg>08jV_KL7v# diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/chardet/__pycache__/johabfreq.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/chardet/__pycache__/johabfreq.cpython-311.pyc index 9b52a84f6ae143c145d093890b69cd86b6bc8a30..e627a72a57c3eaa96b38a656c7c9319115440720 100644 GIT binary patch delta 25 fcmdlym38w}R_^7zyj%=Gu-kA;BllKr#uZ)wVv+|? delta 25 fcmdlym38w}R_^7zyj%=G@cz)|M((ZLj4QkVX{!hn diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/chardet/__pycache__/johabprober.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/chardet/__pycache__/johabprober.cpython-311.pyc index 5b786a6e316552949eed91c326d4d8fb6af213dd..1c25bb358e7a46cfb26bca15f3912107aea17d09 100644 GIT binary patch delta 20 acmbQpJCT=rIWI340}$*s+_I6ojST=Xd<6Rd delta 20 acmbQpJCT=rIWI340}#AFw0R?U8yf&NNd>(kM-m{R_^7zyj%=Gu-kA;BllKrMmv81XJQ8_ delta 25 fcmX>(kM-m{R_^7zyj%=G@cz)|M((ZLjCTG2ZhHsq diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/chardet/__pycache__/langgreekmodel.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/chardet/__pycache__/langgreekmodel.cpython-311.pyc index 34c07a22b70108dec404da6fb3056abd3f0fce78..966431db430d3792bab228ffcb7577edb0937758 100644 GIT binary patch delta 25 fcmbRKnq~TH7VhP|yj%=Gu-kA;BllKr#vW4uY!nBP delta 25 fcmbRKnq~TH7VhP|yj%=G@cz)|M((ZLj6J3Rb1eu} diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/chardet/__pycache__/langhebrewmodel.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/chardet/__pycache__/langhebrewmodel.cpython-311.pyc index 413e0368637589219e971110de2404ecf9352029..04345557259a1e6d33892d391bf326310874f162 100644 GIT binary patch delta 25 fcmbREk!9jX7VhP|yj%=Gu-kA;BllKr#x@H8YySt1 delta 25 fcmbREk!9jX7VhP|yj%=G@cz)|M((ZLjBOSGa~KFx diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/chardet/__pycache__/langrussianmodel.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/chardet/__pycache__/langrussianmodel.cpython-311.pyc index 20a2e62d228b15917f968fc337948b7c82f538ac..a85837faa8d8866ee7afaf3adad55dd2e2d70f65 100644 GIT binary patch delta 25 fcmdmYk!{~aHtyxTyj%=Gu-kA;BllKr#?31LZ^8&k delta 25 gcmdmYk!{~aHtyxTyj%=G@cz)|M((ZLjGI>g0Cw964FCWD diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/chardet/__pycache__/langthaimodel.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/chardet/__pycache__/langthaimodel.cpython-311.pyc index 620969aeeee077b11cdcfa6ac17c33b5f1b39957..a525be556c4d2d657d361d0f88a392b0297dc2aa 100644 GIT binary patch delta 25 fcmX^7iRJJo7VhP|yj%=Gu-kA;BllKrMhi;-biW6u delta 25 fcmX^7iRJJo7VhP|yj%=G@cz)|M((ZLj24yvd)NqT diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/chardet/__pycache__/langturkishmodel.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/chardet/__pycache__/langturkishmodel.cpython-311.pyc index e3fcea1052c9e08b4825af7e464d9396bdf9f9c8..064fcc33f8f3166d613a25c8771a09906f435757 100644 GIT binary patch delta 25 fcmZ4Vk!8_G7VhP|yj%=Gu-kA;BllKr#_1LSZ$JmZ delta 25 fcmZ4Vk!8_G7VhP|yj%=G@cz)|M((ZLjMFUuc3B98 diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/chardet/__pycache__/latin1prober.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/chardet/__pycache__/latin1prober.cpython-311.pyc index a8516e329d0e947bf2cf1f3a19313529843881b2..344c00c773e191247fd3d306a6c3935e5ff4d9be 100644 GIT binary patch delta 20 acmZ2vxyX`xIWI340}$*s+_I5-x(on1Y6YAC delta 20 acmZ2vxyX`xIWI340}#AFw0R@pw*UY*#|3@> diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/chardet/__pycache__/mbcsgroupprober.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/chardet/__pycache__/mbcsgroupprober.cpython-311.pyc index adb9160da45d1419b6459d85140c6cf380c1c1e1..73bcae101397816927302adb07704b8d8b4e36e9 100644 GIT binary patch delta 20 acmX@Ye}tcVIWI340}$*s+_I5-Cp!Q+y#<*7 delta 20 acmX@Ye}tcVIWI340}#AFw0R@QwHAv diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/chardet/__pycache__/utf1632prober.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/chardet/__pycache__/utf1632prober.cpython-311.pyc index 834ba6b478aaa3838b75af2a7b4a8926008093e7..13dcb207fb23be0c49b8f690f10fbf578c847e35 100644 GIT binary patch delta 20 acmcZ{bUBE7IWI340}$*s+_I6|LlXc*qXq5& delta 20 acmcZ{bUBE7IWI340}#AFw0R@9hb90?a0apf diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/chardet/__pycache__/utf8prober.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/chardet/__pycache__/utf8prober.cpython-311.pyc index debadde78ad4ec5c0c5ced640b7c2cb6ae8b8a81..48afd3a8171dbbfa86de078c337fd58a7366aabe 100644 GIT binary patch delta 20 acmeB@?vmzS&dbZi00g@Yw`}CD=LG;VV+9ES delta 20 acmeB@?vmzS&dbZi00i$3ZQjUT&kF!IFa^y3 diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/chardet/__pycache__/version.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/chardet/__pycache__/version.cpython-311.pyc index 6b66f858cfa2db54afde0fa7b960555f4ea37de3..cb9cb1cafd25133ff91c9d12696c8067404564d9 100644 GIT binary patch delta 20 acmeyy{EeA=IWI340}$*s+_I7TH6s8&*9H*) delta 20 acmeyy{EeA=IWI340}#AFw0R@qz2Uh diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/distlib/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/distlib/__pycache__/__init__.cpython-311.pyc index 94b5735bae195e8fc05a05957ae52f335adbc3af..8f996752865af49c6e7925c972c35d29c59463f4 100644 GIT binary patch delta 20 acmZ3$y?~p0IWI340}$*s+_I5-Dk}gpiv=S9 delta 20 acmZ3$y?~p0IWI340}#AFw0R@mf^M(*Xjyj%=Gu-kCUMs6i9089o3BLDyZ delta 22 ccmZ2Gg>mf^M(*Xjyj%=G@cz)|joeCJ08rBg=Kufz diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/distlib/__pycache__/scripts.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/distlib/__pycache__/scripts.cpython-311.pyc index 3b67fa18140f9012be93e44700bdb5f6b9086f69..d9d93125c2eea3435d40f94e6d45066618414824 100644 GIT binary patch delta 22 ccmbQZjB(;JM(*Xjyj%=Gu-kCUMsBuX07@AJ`~Uy| delta 22 ccmbQZjB(;JM(*Xjyj%=G@cz)|jofU(08ZuxzyJUM diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/distlib/__pycache__/util.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/distlib/__pycache__/util.cpython-311.pyc index 56f2103a217b021fc6cecab482a89851d73a24fb..b31250e23b43457921a12f99aafd7c667832bbab 100644 GIT binary patch delta 29 kcmZ4TlXb~YR_^7zyj%=Gu-kCUM((MP7=LYE_=tHH0HU}HyZ`_I delta 29 kcmZ4TlXb~YR_^7zyj%=G@cz)|joecoG2Ys|@DcMY0IVSlU;qFB diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/distro/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/distro/__pycache__/__init__.cpython-311.pyc index c054d0aa7f2129609fa5818f4c6b4e5c661873b5..48bd30b24c58f7c0af6e6481a0f8566ee9eb72ab 100644 GIT binary patch delta 20 acmZ3JV delta 20 acmbO!FjIhgIWI340}#AFw0R>p9|r(7_XS@7 diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/msgpack/__pycache__/exceptions.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/msgpack/__pycache__/exceptions.cpython-311.pyc index a1935d525dd6cddf71a791efbefedb64b35bcc69..bd9e06e67883131faac5337958943504114e826a 100644 GIT binary patch delta 20 acmX>mbWDhQIWI340}$*s+_I6|niBvydj%%| delta 20 acmX>mbWDhQIWI340}#AFw0R@9H75W*NCoQv diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/msgpack/__pycache__/ext.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/msgpack/__pycache__/ext.cpython-311.pyc index 470772b6102e13a883dcf010790fc7cbecdbe711..e60632b737956c5353f81b1869417cdf1e6eca29 100644 GIT binary patch delta 20 acmX@%e!`u5IWI340}$*s+_I5-uQC8aSq3ox delta 20 acmX@%e!`u5IWI340}#AFw0R@C delta 20 acmaDT^H7F+IWI340}#AFw0R?U7#{#b?FLl< diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/packaging/__pycache__/markers.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/packaging/__pycache__/markers.cpython-311.pyc index 52d655d31b1454a316502031b0aacba16ac47be7..413ef6f3e9fd1ab163c6166b14fc4baa7486ae21 100644 GIT binary patch delta 22 ccmeBdWbABY$X(|E07%#d%K!iX diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/packaging/__pycache__/requirements.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/packaging/__pycache__/requirements.cpython-311.pyc index cbbf18adb25b40b6c6fd161e9a29d6743f0b58e3..918e0129aa06aae727d21c15e1cb4b06013e9b9f 100644 GIT binary patch delta 20 acmca&eZ`u4IWI340}$*s+_I7Tq$~hJg9bMM delta 20 acmca&eZ`u4IWI340}#AFw0R@^9u8kz1z?08HHmZvX%Q delta 22 ccmdnq!?eAJiF-LOFBbz4yg#&gBezZ)08y$3GXMYp diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/packaging/__pycache__/tags.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/packaging/__pycache__/tags.cpython-311.pyc index 118dc27b7544b53fd3671d5d65420ef763a9b050..12e0e1d3cc30b2cfc793f033c21a237baa2386db 100644 GIT binary patch delta 22 ccmaE`jPb!TM(*Xjyj%=Gu-kCUM()sH09RfH=>Px# delta 22 ccmaE`jPb!TM(*Xjyj%=G@cz)|johKZ09-2vtpET3 diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/packaging/__pycache__/utils.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/packaging/__pycache__/utils.cpython-311.pyc index b515b70101937ca97b606ae46bf6a8cea53f8bb3..49121ed963bd0073c36e2f9e12c59c60a8135d46 100644 GIT binary patch delta 20 acmbPkGTnrGIWI340}$*s+_I6IM+yKo`~>d+ delta 20 acmbPkGTnrGIWI340}#AFw0R>pj}!nq$py0j diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/packaging/__pycache__/version.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/packaging/__pycache__/version.cpython-311.pyc index 6fb715b94975297843786147eb4a470d951b669d..adb98dd388424c771103fbebebf8f0143cfeb90e 100644 GIT binary patch delta 22 ccmaFAit+s_M(*Xjyj%=Gu-kCUM(&hQ09t_uAOHXW delta 22 ccmaFAit+s_M(*Xjyj%=G@cz)|joc}r0AEfA&*1PVVKryj%=Gu-kA;BX=t|<5q4ajw}Fucn7Ee delta 28 icmbPyg>&*1PVVKryj%=G@cz)|M($Q_#;x2;99aN{-3V*| diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/pkg_resources/__pycache__/py31compat.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/pkg_resources/__pycache__/py31compat.cpython-311.pyc index d3ebdee376842367acef68bbb5f62a3e2acb0c46..215e1217a8c267ddced45687b7fda49590359f05 100644 GIT binary patch delta 20 acmcb@eubTTIWI340}$*s+_I7TBr^az83nZf delta 20 acmcb@eubTTIWI340}#AFw0R@;{|w delta 20 acmeyD@-KyZIWI340}#AFw0R?Uu@L}Lxd&hX diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/platformdirs/__pycache__/api.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/platformdirs/__pycache__/api.cpython-311.pyc index 3120910f5bf341b42a08da33110f8c298367505f..7792727dc7614c16154e0c8e69efc0521f221b45 100644 GIT binary patch delta 20 acmeCN=&|5l&dbZi00g@Yw`}BQlmP%W)&$r9 delta 20 acmeCN=&|5l&dbZi00i$3ZQjVuC<6dGqXnD* diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/platformdirs/__pycache__/unix.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/platformdirs/__pycache__/unix.cpython-311.pyc index a44cf26b108c88c5207c1585fe1b4a7e3948e124..3666e993530b8c93250405cf59422c3d67ee2d9a 100644 GIT binary patch delta 20 acmeAR>kH#v&dbZi00g@Yw`}BQ)&>ANZ3QU+ delta 20 acmeAR>kH#v&dbZi00i$3ZQjVutPKD^ItA?j diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/platformdirs/__pycache__/version.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/platformdirs/__pycache__/version.cpython-311.pyc index 51e211a503e4eb80e951a7d470d9cc44f8bb94ff..e094246c36fe0bcd834e083de5ee5129b23e69f2 100644 GIT binary patch delta 20 acmdnWw3Uf_IWI340}$*s+_I5dixB`civ-L7 delta 20 acmdnWw3Uf_IWI340}#AFw0R@979#*TSOt&( diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/pygments/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/pygments/__pycache__/__init__.cpython-311.pyc index eea6404bad2e76f1cea6ced29366d0bd3e0c2e3d..bea8b9ad1e1ffecfcfa80da65bdb826a807d0030 100644 GIT binary patch delta 20 acmZpaYn0<&&dbZi00g@Yw`}D8#s>g0Xa#iu delta 20 acmZpaYn0<&&dbZi00i$3ZQjWJjSm1fH3m5V diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/pygments/__pycache__/filter.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/pygments/__pycache__/filter.cpython-311.pyc index a1919efee3b066c387140b3ebd54d24d94600276..75655d19dec3a8c7615d942db54fdd6ecde3b110 100644 GIT binary patch delta 20 acmZ20y;hogIWI340}$*s+_I5-5ibBY+XZp} delta 20 acmZ20y;hogIWI340}#AFw0R@ delta 22 dcmX@To9Xm!Chq0Dyj%=G@cz)|job(30svh`2xI^N diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/pygments/__pycache__/modeline.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/pygments/__pycache__/modeline.cpython-311.pyc index c6f1a00b55d2dba48a8f56b42461d8f7c186e4e2..4754a1d5a190a214a74bc1654c843368f03c94e7 100644 GIT binary patch delta 20 acmdnNyMvc|IWI340}$*s+_I5-EgJwglm&1A delta 20 acmdnNyMvc|IWI340}#AFw0R@)~)CKGS diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/pygments/__pycache__/util.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/pygments/__pycache__/util.cpython-311.pyc index 6dbfc05db70be5a3b137ef8b58cd142393cacc07..f6cb42967c7f6605cfffb77c3f582d8c71cd728b 100644 GIT binary patch delta 20 acmexU_@|J2IWI340}$*s+_I7TqXhs^b_X^9 delta 20 acmexU_@|J2IWI340}#AFw0R@50035%2kig= diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/pygments/lexers/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/pygments/lexers/__pycache__/__init__.cpython-311.pyc index 489add39342d99f1ea4204804dfcfc840e1a8b41..31469c7d2469411a0a8280a20b769b4c3000ec20 100644 GIT binary patch delta 20 acmZ2cwxW!CIWI340}$*s+_I5d)*1jq4F$&l delta 20 acmZ2cwxW!CIWI340}#AFw0R@9tTg~f+6IdN diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/pygments/lexers/__pycache__/_mapping.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/pygments/lexers/__pycache__/_mapping.cpython-311.pyc index d41c6dc500c58da900e2bf7161dc849250fc128a..db7c13e831bcd05977e3a56513fb30c398c2f2c6 100644 GIT binary patch delta 22 ccmdn~ih27hX71&@yj%=Gu-kCUMsA(209(TcoB#j- delta 22 ccmdn~ih27hX71&@yj%=G@cz)|jodn40bq~_U;qFB diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/pygments/styles/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/pygments/styles/__pycache__/__init__.cpython-311.pyc index f487a0ce15487d0243a8f48dd6b0f18638b1998b..8b04e5ef8c308eea006f2dfa4b03d52c179d418b 100644 GIT binary patch delta 20 acmaE+^h}9+IWI340}$*s+_I57S`Yw0!v(JZ delta 20 acmaE+^h}9+IWI340}#AFw0R?Uv>*UQkOp%A diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/pyparsing/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/pyparsing/__pycache__/__init__.cpython-311.pyc index dab9f6d3667497624a05b038ca75d8053932010f..6fe3540fd7e18f25a461127ddf2e0745cc2c7cbe 100644 GIT binary patch delta 20 acmbQ?IKz>9IWI340}$*s+_I6oR{;Py{soc% delta 20 acmbQ?IKz>9IWI340}#AFw0R?UuL1x*%LY~e diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/pyparsing/__pycache__/actions.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/pyparsing/__pycache__/actions.cpython-311.pyc index 2372f2f717caf8a8c8b14562c0cb32cd884a51fd..148b759802c595931a13342edbab71dc0084d01c 100644 GIT binary patch delta 20 acmeBj>T=><&dbZi00g@Yw`}D8uK)lz@&)Pu delta 20 acmeBj>T=><&dbZi00i$3ZQjWJUjYC;zXq-V diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/pyparsing/__pycache__/common.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/pyparsing/__pycache__/common.cpython-311.pyc index 449ff5fa50c10f725bd12e4736e856b4f96ea0d4..f7d8723ed1ac0859cef13ae1cd012be277a0fbab 100644 GIT binary patch delta 20 acmdl}yr-CZIWI340}$*s+_I5-qa^@FzXp2% delta 20 acmdl}yr-CZIWI340}#AFw0R@MmtKPmuEV-X7g delta 43 zcmZ4VQ()0g0q*6zyj%=G@cz)|M($Q_#;x2;vk$Y}VywQ^zUna3_Em?Oe^dYfVI&cc diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/pyparsing/__pycache__/exceptions.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/pyparsing/__pycache__/exceptions.cpython-311.pyc index 104db7946f2ec89346311d5c1acf3486fc4c1cd5..eda348a97bff0f6fd40be923ad451d148754519b 100644 GIT binary patch delta 20 acmeyB@+*aVIWI340}$*s+_I57-v|IrOa_nu delta 20 acmeyB@+*aVIWI340}#AFw0R?Uz7YUY83$AV diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/pyparsing/__pycache__/helpers.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/pyparsing/__pycache__/helpers.cpython-311.pyc index 48a7b17a86dfeb223913d983dc0ea5a09864fb39..129dce09d785cd9e0ccc68a1a4dad4f2883ccaac 100644 GIT binary patch delta 22 ccmeyqi23^>X71&@yj%=Gu-kCUM(&)809$7Vr~m)} delta 22 ccmeyqi23^>X71&@yj%=G@cz)|jodjG0bn!;YybcN diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/pyparsing/__pycache__/results.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/pyparsing/__pycache__/results.cpython-311.pyc index 3c4412317c741c0c0f0f1e1495a9dfa908938339..920f2ad2e62bf715343ce86ba63a4bf6e7f9c8b5 100644 GIT binary patch delta 22 ccmcaKo9WVQChq0Dyj%=Gu-kCUM(*Ri093XIWdHyG delta 22 ccmcaKo9WVQChq0Dyj%=G@cz)|join30a=3xDF6Tf diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/pyparsing/__pycache__/testing.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/pyparsing/__pycache__/testing.cpython-311.pyc index 09713b38579640bffe84669ba2a6216f423eb94a..1b69d4cf213d25635535a7357814b60a33b3d384 100644 GIT binary patch delta 22 ccmZ2CgK^yqM(*Xjyj%=Gu-kCUMs8&v08GyYFaQ7m delta 22 ccmZ2CgK^yqM(*Xjyj%=G@cz)|joivU08yL<^Z)<= diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/pyparsing/__pycache__/unicode.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/pyparsing/__pycache__/unicode.cpython-311.pyc index 9cd5958a6d14a9a6af81bcbf17ecc59ca0fbd0dc..6051c25e2f1ea6e4f2274cd2b71751b9b98b9159 100644 GIT binary patch delta 20 acmZpuXsF;`&dbZi00g@Yw`}D8Y7GEC6$VoP delta 20 acmZpuXsF;`&dbZi00i$3ZQjWJ)fxap;s+N1 diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/pyparsing/__pycache__/util.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/pyparsing/__pycache__/util.cpython-311.pyc index bf373e93fd2aad7c2745e26ea78e53e6da3bfcea..d198977029249b3e123d66fed02f4321ca38b427 100644 GIT binary patch delta 20 acmdm-zd4_KIWI340}$*s+_I5-g*gC3#RgUY delta 20 acmdm-zd4_KIWI340}#AFw0R@<3UdHSk_Q?9 diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/pyproject_hooks/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/pyproject_hooks/__pycache__/__init__.cpython-311.pyc index d60394a5f0a34b8f7d59d725ae9ba63d59e005c3..67fc0d37065dc3307d5e39bde016f9a112d87564 100644 GIT binary patch delta 20 acmdnTx{sB6IWI340}$*s+_I5-GZO$eU^9u8k(=KU07ec4y#N3J delta 22 ccmbQ$#5k*ok$X8WFBbz4yg#&gBR9Vz07}~ifdBvi diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/pyproject_hooks/_in_process/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/pyproject_hooks/_in_process/__pycache__/__init__.cpython-311.pyc index c64ef011dd9c214d31f0831023982812d9cd0f06..5305d4cc9e8e444e0494582fbe60859270edecd9 100644 GIT binary patch delta 20 acmeC;?Be8J&dbZi00g@Yw`}CDX8`~&(gfT9 delta 20 acmeC;?Be8J&dbZi00i$3ZQjUT&jJ86p9P=* diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/requests/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/requests/__pycache__/__init__.cpython-311.pyc index 5db8b3c0f123be34407502c8ba39fe74a2cb6953..32427e6d66fe3834510e8c098961f183604f5d19 100644 GIT binary patch delta 20 acmZ2yw9bfoIWI340}$*s+_I5dSrPy`O9d|g delta 20 acmZ2yw9bfoIWI340}#AFw0R@9vLpaM7zOhH diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/requests/__pycache__/__version__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/requests/__pycache__/__version__.cpython-311.pyc index ed12c0daa965e69b76f7354665751b89166bb198..2d8443d15b327c051a6333d829111a4bfd3d2f9f 100644 GIT binary patch delta 20 acmX@ka-4;GIWI340}$*s+_I6|h6w;Sqy+~6 delta 20 acmX@ka-4;GIWI340}#AFw0R@94HE!7aRti& diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/requests/__pycache__/_internal_utils.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/requests/__pycache__/_internal_utils.cpython-311.pyc index c4d4468fdfd782c0a731559572eccdcc544b8a67..761cdd98b45e757c30410316a318ad54f81ce5e7 100644 GIT binary patch delta 20 acmZ1=ut0!&IWI340}$*s+_I5dlmh@WLIkh? delta 20 acmZ1=ut0!&IWI340}#AFw0R@9C>*eEK&dbZi00g@Yw`}BQVg~>)0R(aY delta 20 acmeC>>*eEK&dbZi00i$3ZQjVu#0~&7&IL9A diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/requests/__pycache__/cookies.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/requests/__pycache__/cookies.cpython-311.pyc index 1e0ec7868ff202188f074d720febb24c488c74ca..e06c6db6d20987b790a0c3112e48e60e584b527a 100644 GIT binary patch delta 22 ccmaESnep*uM(*Xjyj%=Gu-kCUM(!J#0AJ7t;s5{u delta 22 ccmaESnep*uM(*Xjyj%=G@cz)|joddf0c4#BrT_o{ diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/requests/__pycache__/exceptions.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/requests/__pycache__/exceptions.cpython-311.pyc index ac9d168769516802e1e9c7370bd45c464ec1d43e..b66fca5497d388bb9b7dc9924b89a632d4174c8e 100644 GIT binary patch delta 20 acmX@*bjpc)IWI340}$*s+_I6|UJ(F5^98E_ delta 20 acmX@*bjpc)IWI340}#AFw0R@9y&?cbzy@ys diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/requests/__pycache__/hooks.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/requests/__pycache__/hooks.cpython-311.pyc index b34de50d578ec3a2823b71c3ddd76ee65150a1f4..4346423e0611d93dc388f139accc53c72994db7a 100644 GIT binary patch delta 20 acmcc5d7qPeIWI340}$*s+_I7T3JU-|8U@<` delta 20 acmcc5d7qPeIWI340}#AFw0R@<6&3(N=LVku diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/requests/__pycache__/models.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/requests/__pycache__/models.cpython-311.pyc index 876ebc06914a1110c1d7e537a807a8b76fc61a60..1e77ec329f7be3d0770e2c098aefbe3103f1cc77 100644 GIT binary patch delta 22 ccmeyhj_KDrChq0Dyj%=Gu-kCUM(+IS09{=NjQ{`u delta 22 ccmeyhj_KDrChq0Dyj%=G@cz)|jokUu0b(i$Q2+n{ diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/requests/__pycache__/packages.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/requests/__pycache__/packages.cpython-311.pyc index 6ac9ed9e77e26eed850e81baa85fd73978070d07..f69ab57c343204880735dfc5683b3fe173ab3281 100644 GIT binary patch delta 20 acmdnZwwsN6IWI340}$*s+_I6|fEfTb4Fu=_ delta 20 acmdnZwwsN6IWI340}#AFw0R@90W$zP+6Alt diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/requests/__pycache__/sessions.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/requests/__pycache__/sessions.cpython-311.pyc index afdc420d05754ec8a15a96b8a3aa3faf6a58f39b..1ea6d5ef1e267b95ede620ed1ba0e65d6a561125 100644 GIT binary patch delta 22 ccmdn+oN>c*M(*Xjyj%=Gu-kCUM((A>09sWCdjJ3c delta 22 ccmdn+oN>c*M(*Xjyj%=G@cz)|joeF%0be2rKL7v# diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/requests/__pycache__/status_codes.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/requests/__pycache__/status_codes.cpython-311.pyc index 80d308962aab57d3a7416c89919aa4a94a4e7af7..396f0b9204242b84658a5c71ebdf23c48bb4d7e1 100644 GIT binary patch delta 20 acmca*aLa&uIWI340}$*s+_I6|UjhI?;sve% delta 20 acmca*aLa&uIWI340}#AFw0R@9zXSk9uLg1e diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/requests/__pycache__/structures.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/requests/__pycache__/structures.cpython-311.pyc index 59d9b7f657352a82ea0bbd25274255a18c7789bb..c65b7b4dd08d09c07918994cb1890739372cc137 100644 GIT binary patch delta 20 acmX?YaN2--IWI340}$*s+_I6|K>`3iiUolH delta 20 acmX?YaN2--IWI340}#AFw0R@9g9HFWR|Z7@ diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/requests/__pycache__/utils.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/requests/__pycache__/utils.cpython-311.pyc index 60abd6d7b4e26ffdbcae3a12de0be15ec17e51e1..002cfcbf639b0cc494fa1316b60da6d16dce6b16 100644 GIT binary patch delta 22 ccmX@Qlj-PAChq0Dyj%=Gu-kCUM($m609Pmni2wiq delta 22 dcmX@Qlj-PAChq0Dyj%=G@cz)|joiED003L{2u%P0 diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/resolvelib/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/resolvelib/__pycache__/__init__.cpython-311.pyc index 6d5af1a36e10c7f6a341c4731ec2a02191daf8c4..50974550380420cadc101bc9372d60e3eae03800 100644 GIT binary patch delta 20 acmaFI`i_-*IWI340}$*s+_I7TF%tkj;068w delta 20 acmaFI`i_-*IWI340}#AFw0R@sX diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/resolvelib/__pycache__/providers.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/resolvelib/__pycache__/providers.cpython-311.pyc index 75d6b32f4c072e4a99d3f14b1f16c1afc4257e25..3856fb4a2daf9ba8a773150762324b8a77160d8c 100644 GIT binary patch delta 20 acmbPhKG&RkIWI340}$*s+_I5-k~9E0Sp|py delta 20 acmbPhKG&RkIWI340}#AFw0R@b%7 diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/resolvelib/__pycache__/structs.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/resolvelib/__pycache__/structs.cpython-311.pyc index 585bb10753f934afbf80d3d09d056b28a0c45c28..1372e67cb2057cd1739d39a4a04dd102245545ee 100644 GIT binary patch delta 20 acmdlVu|I-)IWI340}$*s+_I6|L^9ue$lc1#*viecm7BRn4*-?s2y*}c delta 31 lcmZp_%hP_BhkH3MFBbz4yg#(Lk-L?fv6Y)?D>rkE9sr@22{!-$ diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/_emoji_replace.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/_emoji_replace.cpython-311.pyc index d11d64e0e3cd4d481149dd676c481a74e2901014..c743852112d04e8e7a488752c29ed4f99f6e804c 100644 GIT binary patch delta 20 acmeC?@8;)T&dbZi00g@Yw`}BYUga6o{2IWI340}$*s+_I6|lmh@bLIn^2 delta 20 acmX>ga6o{2IWI340}#AFw0R@9DF*;O4+Yc! diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/_null_file.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/_null_file.cpython-311.pyc index 332f083991ab8af9dc7444a21071b3b9c24dce22..492191660f210ff59ddd25a69ac7c4c34de5eee4 100644 GIT binary patch delta 20 acmX@Aa#V$TIWI340}$*s+_I6|N(cZupA2R?oiUmdh diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/_ratio.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/_ratio.cpython-311.pyc index b9a3a9b034371fdadd020a4b352bac834b595c2d..366cb84cea74841c55c1a97abbb32db53000cb06 100644 GIT binary patch delta 20 acmexu``ea#IWI340}$*s+_I7TgB$=zn diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/_wrap.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/_wrap.cpython-311.pyc index 73362e19fc343d7c7da92a879f4711479a72f54b..4c81a42504c3dad38efd72bd2d671a2297bfaacc 100644 GIT binary patch delta 20 acmcaFdS8@#IWI340}$*s+_I7T3KsxA&;|Jb delta 20 acmcaFdS8@#IWI340}#AFw0R@<6)pfnod&%C diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/abc.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/abc.cpython-311.pyc index ca30758ff1ae927b53f4c527c84822c51bdc0b9f..fed08e09cecf381bfd0e361329d05a5c3b3286dd 100644 GIT binary patch delta 20 acmZqSZ{g=&&dbZi00g@Yw`}CDWCs8*y#(0+ delta 20 acmZqSZ{g=&&dbZi00i$3ZQjUT$qoQCiUpkj diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/align.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/align.cpython-311.pyc index 87e4ea552d70877edac1e8ed4d3331dbc9a6aa4e..12e603d7239d8f69a773d1ed1a8d9bf5541a6a2b 100644 GIT binary patch delta 20 acmZ3OxiFJ^IWI340}$*s+_I5-nh5|w8U_&n delta 20 acmZ3OxiFJ^IWI340}#AFw0R@cY%+4IWI340}$*s+_I6|l^p;&c?CNF delta 20 acmcb>cY%+4IWI340}#AFw0R@9D?0!`Mg{)> diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/columns.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/columns.cpython-311.pyc index 7f2f154b4df2234e1da7d0549e8585b2ce58f925..7fb33938503e71da3a2e476d92d3391e357d7aab 100644 GIT binary patch delta 20 acmbOnJUN(qIWI340}$*s+_I6oLlXcy#|5we delta 20 acmbOnJUN(qIWI340}#AFw0R?Uhb90(lm>JF diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/console.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/console.cpython-311.pyc index d301a4a4a8d7d30838177172515b0c298b5700ec..036edb8fdd373cd91c79383966fb29eacab7c5fd 100644 GIT binary patch delta 33 ocmbP!h<*AYcJAf8yj%=Gu-kA;BllKr#_i>dzqTJPXAH>%0JU!mTmS$7 delta 33 ocmbP!h<*AYcJAf8yj%=G@cz)|M((ZLjN8i@Z*4zZ&KQyj0Kv!%00000 diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/constrain.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/constrain.cpython-311.pyc index 7e7b7f80cc16039f71b2fbd24572604d829974f8..97bd1d63dbc87ef0403432a95206d9d7baa9b602 100644 GIT binary patch delta 20 acmZ1=yg-+P6mnq delta 20 acmcbRcp;H{IWI340}#AFw0R@8wXAR diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/emoji.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/emoji.cpython-311.pyc index f8950a7317ec65cd0f20e892d5ba6b034378aab9..19136b4123381c83567c0af5c157c5b6938ca865 100644 GIT binary patch delta 20 acmdn1x>uEZIWI340}$*s+_I5-lMnzq$OXFq delta 20 acmdn1x>uEZIWI340}#AFw0R@pFDC#u7zJSf diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/file_proxy.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/file_proxy.cpython-311.pyc index 13e1d568398895d2296494c8d76787907a106c0c..0375444ab691427ce0413dd9ff0c5139a68ddd3b 100644 GIT binary patch delta 20 acmX>gdq9?ZIWI340}$*s+_I5-D<1$m90jld delta 20 acmX>gdq9?ZIWI340}#AFw0R@~KF diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/filesize.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/filesize.cpython-311.pyc index 8fbf9625cd5afc5853dc0ab1d1eaf137144c9092..bfa542d914c8b90be48da37d25238ae94720039a 100644 GIT binary patch delta 20 acmaDN`9zX?IWI340}$*s+_I7TCJz8WDh3h& delta 20 acmaDN`9zX?IWI340}#AFw0R@k$X8WFBbz4>^9u8k-H%X07>r#X8-^I delta 22 ccmeBP%GkY>k$X8WFBbz4yg#&gBX>g(08YFID*ylh diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/live_render.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/live_render.cpython-311.pyc index 7085e8e6af91507952d8f97f51c535e2f122e930..6f027aa97b5ac069126b520c18d6aecb655e5493 100644 GIT binary patch delta 20 acmbQCF++oUIWI340}$*s+_I6IR|EhxZUoc- delta 20 acmbQCF++oUIWI340}#AFw0R>puLuA-I|Y~k diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/logging.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/logging.cpython-311.pyc index 034a44c2f2e57919c87349821649877ac0116183..9d2480764f9863e366a0e16de8503f57a020883e 100644 GIT binary patch delta 20 acmdm7xVeyfIWI340}$*s+_I5-g#`db4hCBQ delta 20 acmdm7xVeyfIWI340}#AFw0R@<3JU;9+Xo*2 diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/markup.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/markup.cpython-311.pyc index 8952ac22ae1c78defe727dd93b59820f097aa3af..7ed542d7a49e0c949767b3711b10df054018f74d 100644 GIT binary patch delta 20 acmX>acr=iEIWI340}$*s+_I5-mj(bsP6j~$ delta 20 acmX>acr=iEIWI340}#AFw0R@e delta 20 acmaE6@yvpIIWI340}#AFw0R?UvWyaqP_ diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/pager.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/pager.cpython-311.pyc index 91146ad835eda5a77e88a0c984e3182917c4a814..2fa93d91168e1dd1d8da03fd4220810beed0df90 100644 GIT binary patch delta 20 acmX>ictntUIWI340}$*s+_I5-CkFsJ1_hn~ delta 20 acmX>ictntUIWI340}#AFw0R@M9IWI340}$*s+_I5-lOX^^p9WR{ delta 20 acmdm+yf>M9IWI340}#AFw0R@rEWa45e{H^JYf}gSxO)sD delta 32 ncmbPrk7?FDChq0Dyj%=G@cz)|jojyLS#B{_-`af7)}{~u!w?Ln diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/progress.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/progress.cpython-311.pyc index c7d65e4aa87da4a926fab0e9479b7639759896a0..c1c423862bba6e74891743d81aa04ffaa088fe74 100644 GIT binary patch delta 25 fcmbQ(#yYW$m3uiaFBbz4>^9ue$i0=DkufdX9b}E diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/region.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/region.cpython-311.pyc index efb2ad167a5917f9a51526b7183060b420de68b0..c880a577016785a491fcbc552f59f4d77fc9d7a8 100644 GIT binary patch delta 20 acmbQlI*FBgIWI340}$*s+_I6ooe2OifCTCQ delta 20 acmbQlI*FBgIWI340}#AFw0R?UI}-pkO$Dw1 diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/repr.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/repr.cpython-311.pyc index 52e42b2abdfe42db74df193d2824e4174c4a1cb2..014ce47fe926049a9ce8cb389126a6c5ddf67a4e 100644 GIT binary patch delta 20 acmaE9{nDCyIWI340}$*s+_I7To-6=G%LaA; delta 20 acmaE9{nDCyIWI340}#AFw0R@^9u8k(<8_07w1?7ytkO delta 22 ccmbQ$!!)ahiF-LOFBbz4yg#&gBR78=08GmU+yDRo diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/styled.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/styled.cpython-311.pyc index 99cf3a72f40497562a831c70746ef29797bc7f87..108e265adc42a742c92fe3c96e052c31636e3081 100644 GIT binary patch delta 20 acmZn@ZWHES&dbZi00g@Yw`}CD<^%vRp9JXu delta 20 acmZn@ZWHES&dbZi00i$3ZQjUT%?SWCYz3_V diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/syntax.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/syntax.cpython-311.pyc index 1c170046393ce954a00029bf745f7a06833783e4..64a0c0427a2e233f2d608ca604019df039321e6f 100644 GIT binary patch delta 22 ccmZ2-hH1$eChq0Dyj%=Gu-kCUMsCSv08@+x)&Kwi delta 22 ccmZ2-hH1$eChq0Dyj%=G@cz)|jogyU09aWEng9R* diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/table.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/table.cpython-311.pyc index de5d9491ddb4c07ea0a68d920a314b83b2d073ca..de34e7672a226216ce3e45ad84a15aa4e1abe6fb 100644 GIT binary patch delta 22 ccmbRLmudcAChq0Dyj%=Gu-kCUM(!#509!-{!2kdN delta 22 dcmbRLmudcAChq0Dyj%=G@cz)|joefA0RUhT2!#Lu diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/terminal_theme.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/terminal_theme.cpython-311.pyc index 228f5601e6eb0979d2fbf71ca651160314c71e96..893b9f93a8341e2a839fbf52a084a5153674490b 100644 GIT binary patch delta 20 acmew%^FxMvIWI340}$*s+_I57mk$6!xCPb# delta 20 acmew%^FxMvIWI340}#AFw0R?UE*}6#g$9}c diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/text.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/text.cpython-311.pyc index 43b0644630cab4787f46879c1e1b67b853e04723..1d04068f1c2982fbfd4e63586998907f6a0d6a87 100644 GIT binary patch delta 22 ccmdo0mwEqRX71&@yj%=Gu-kCUM(!>D0AUFTV*mgE delta 22 dcmdo0mwEqRX71&@yj%=G@cz)|joe%Q0RUyz2`2ym diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/theme.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/rich/__pycache__/theme.cpython-311.pyc index 403f5f99c004e96b085fe9f77caff3ffd5bc4d44..f1c13c612256b79b8af5f1a1f56e29a90bdf317b 100644 GIT binary patch delta 20 acmaE6{>+?vIWI340}$*s+_I7Twln}l!3JXh delta 20 acmaE6{>+?vIWI340}#AFw0R@09|-1U!UTW- delta 20 acmbOrFhPKOIWI340}#AFw0R>pD+d5Ij|D^k diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/tenacity/__pycache__/after.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/tenacity/__pycache__/after.cpython-311.pyc index d2656cb3c6ce67e0ec957b9d6b32d007f371ca23..e240ec97fb6bb3c12258236f703ba74e1445a7ab 100644 GIT binary patch delta 20 acmbQuJDZn#IWI340}$*s+_I5-0viA`3tSpa$ju diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/tenacity/__pycache__/before_sleep.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/tenacity/__pycache__/before_sleep.cpython-311.pyc index 29d57d3408b44698b8cc3093d38d22a0c6f2e661..4ff84ceec0dd7c6927cdaa8bdb3f1f48be077dba 100644 GIT binary patch delta 20 acmdlcuuXt_IWI340}$*s+_I5dn*#tgvIOz~ delta 20 acmdlcuuXt_IWI340}#AFw0R@9HU|JZe+9Mx diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/tenacity/__pycache__/nap.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/tenacity/__pycache__/nap.cpython-311.pyc index 92077180d075721f472fe476d8566a24013a9fdf..93ea491786c728a7f23ed4137bf171463ac35253 100644 GIT binary patch delta 20 acmbQkGlz$JIWI340}$*s+_I5dkPQGb5Cob4 delta 20 acmbQkGlz$JIWI340}#AFw0R@9AR7QS-349% diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/tenacity/__pycache__/retry.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/tenacity/__pycache__/retry.cpython-311.pyc index 0bc5ac4e757b51d9996725334cdfe679a26e7dad..6eeab65437daac776f6637a4538e3471fb5fa2c0 100644 GIT binary patch delta 20 acmdm2y04UbIWI340}$*s+_I5-vlReGSO$gw delta 20 acmdm2y04UbIWI340}#AFw0R@GwIWI340}$*s+_I6oj}ZVdPXzh^ delta 20 acmbQqJd>GwIWI340}#AFw0R?UA0q%Z90k4r diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/tomli/__pycache__/_parser.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/tomli/__pycache__/_parser.cpython-311.pyc index a4c1eb5dab5a63eaee2ff6628e9ec4dd6dd99b85..a4864f470d2966300643448f0a5e9544c6e34fdf 100644 GIT binary patch delta 36 pcmZqp$k_0ak$X8WFBbz4>^9u8k^7$~m diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/tomli/__pycache__/_re.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/tomli/__pycache__/_re.cpython-311.pyc index 4709f7629623a61d7c40b29b31f299a5b908b2e2..bda90ee3e4ff2e09313634d9861845a2e3b6f469 100644 GIT binary patch delta 20 acmeBB?oj4l&dbZi00g@Yw`}CD6$Ah?Hw6~} delta 20 acmeBB?oj4l&dbZi00i$3ZQjUTD+mBM1O?jw diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/tomli/__pycache__/_types.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/tomli/__pycache__/_types.cpython-311.pyc index 09181ffcd18abb649ae63d3ee0a536d082048e6c..4b6834adb3c7460664dc443fcb99326449d0b44f 100644 GIT binary patch delta 20 acmbQpJdv4uIWI340}$*s+_I6ojS&Da4+P=> delta 20 acmbQpJdv4uIWI340}#AFw0R?U8zTTT+y$lp diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/urllib3/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/urllib3/__pycache__/__init__.cpython-311.pyc index 3e37f0ded6f8031d62e0ef99efc25244a6689d89..50be2f35e5010f6dcf0062b7e52834f0b28e4f22 100644 GIT binary patch delta 20 acmew@^IL{{IWI340}$*s+_I6ofDZseFa_TL delta 20 acmew@^IL{{IWI340}#AFw0R?U0UrQI{RX1| diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/urllib3/__pycache__/_collections.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/urllib3/__pycache__/_collections.cpython-311.pyc index eaa5c5c20b1cc2af8951caa2cbce7b1a79f979c6..156b527af5ecb365b74de882cf4e98bb2d5ef708 100644 GIT binary patch delta 22 ccmey~$N0UEk$X8WFBbz4>^9u8kvqp709S_w@&Et; delta 22 ccmey~$N0UEk$X8WFBbz4yg#&gBX^EF09;fDwg3PC diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/urllib3/__pycache__/_version.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/urllib3/__pycache__/_version.cpython-311.pyc index 3a2689e917b2d44e848e7efb026808f2da43069e..dfe6dce5a08645c8fad329b309cd8e9f945c3d76 100644 GIT binary patch delta 19 Zcmcb~c$1NPIWI340}$*s+%l2-8~`-&1(g5* delta 19 Zcmcb~c$1NPIWI340}#AFw0R=;IRH5N23Y_A diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/urllib3/__pycache__/connection.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/urllib3/__pycache__/connection.cpython-311.pyc index 7aaca10d0790cacf2e3134bba17bc59be7713f8d..1d2fd5242421ef1e7be859411ccaf6b630d2e814 100644 GIT binary patch delta 22 ccmZo(&Dgw}k$X8WFBbz4>^9u8k-H)k07--fUH||9 delta 22 ccmZo(&Dgw}k$X8WFBbz4yg#&gBX>n808UW{A^-pY diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/urllib3/__pycache__/connectionpool.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/urllib3/__pycache__/connectionpool.cpython-311.pyc index 2f301f6414027ce93a854d68487d6d50302ecef2..30e9da90cee6d7e85222fd3b05a01b1bc555bede 100644 GIT binary patch delta 22 ccmZo##?-WoiF-LOFBbz4>^9u8k^B2307>`<5C8xG delta 22 ccmZo##?-WoiF-LOFBbz4yg#&gBlq`708YgR)Bpeg diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/urllib3/__pycache__/exceptions.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/urllib3/__pycache__/exceptions.cpython-311.pyc index 6e97248e192511e0804164c54b2a737e41502318..c36ec249145ecc180b45c4bf8d184f0715ffe96f 100644 GIT binary patch delta 20 acmexa`?HpNIWI340}$*s+_I7TogDyCv#-y1~LEu diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/urllib3/packages/__pycache__/six.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/urllib3/packages/__pycache__/six.cpython-311.pyc index 6a99cba279613fa9d512d55ae5140169f981b953..4910b24ce0d1bd646a3bec76b14b1ac675854e98 100644 GIT binary patch delta 22 ccmaF&is{`eChq0Dyj%=Gu-kCUM(*UT0A!m7{{R30 delta 22 ccmaF&is{`eChq0Dyj%=G@cz)|joisw0cmIm!vFvP diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/urllib3/util/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/urllib3/util/__pycache__/__init__.cpython-311.pyc index 57b123761d84d93e590c81e3c365e4d2b00ecac0..ed5403d765ac985c869ef7af4ef6a52fc5d4782c 100644 GIT binary patch delta 20 acmeyz^^c2tIWI340}$*s+_I6om=yp)7X`Zj delta 20 acmeyz^^c2tIWI340}#AFw0R?UF)IK?cnFb{Q diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/urllib3/util/__pycache__/queue.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/urllib3/util/__pycache__/queue.cpython-311.pyc index c19981e34048d64939d6da3d8e4c3b1dafcbb574..90de8979643f796ce241cceacd5fc03649d6dfcd 100644 GIT binary patch delta 20 acmcb`eT$oWIWI340}$*s+_I7TJSzY^n+4GT delta 20 acmcb`eT$oWIWI340}#AFw0R@^9u8k$br#082dvhX4Qo delta 22 ccmdng%($tUk$X8WFBbz4yg#&gBlmJg08k1CO8@`> diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/urllib3/util/__pycache__/ssl_match_hostname.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/urllib3/util/__pycache__/ssl_match_hostname.cpython-311.pyc index 686de24b0d2cbe5c984aba57dd4554379af22b6f..cfe0bcd2249afd935b6ca90fd6ab454567328de1 100644 GIT binary patch delta 20 acmZ3gyHuBZIWI340}$*s+_I5-mKXpzECq)E delta 20 acmZ3gyHuBZIWI340}#AFw0R@ diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/urllib3/util/__pycache__/ssltransport.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/urllib3/util/__pycache__/ssltransport.cpython-311.pyc index 47b89d339c8c962bcd4a931a716db2aae46c83de..208c7b7ece40e51668a24d56eaddd7b12bfb9509 100644 GIT binary patch delta 20 acmaDA^(u;cIWI340}$*s+_I57UKapKUj|12 delta 20 acmaDA^(u;cIWI340}#AFw0R?Uyepzcv6t8U_&n diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/urllib3/util/__pycache__/url.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/urllib3/util/__pycache__/url.cpython-311.pyc index 9386961b52d34f2054c2caf44e1cc779cef79119..f92cbfe8cc4bd05ac8179ea5729aa9214b136ff0 100644 GIT binary patch delta 22 ccmbQ%$vCZ(k$X8WFBbz4>^9u8k-OUk07rHPLI3~& delta 22 ccmbQ%$vCZ(k$X8WFBbz4yg#&gBX_q808B#%1^@s6 diff --git a/ansible/lib/python3.11/site-packages/pip/_vendor/urllib3/util/__pycache__/wait.cpython-311.pyc b/ansible/lib/python3.11/site-packages/pip/_vendor/urllib3/util/__pycache__/wait.cpython-311.pyc index eb35b82bd618bbbd61445b553195b5513f69a99d..19aa631bbe9f7e41f54b4554c47dbd848751c152 100644 GIT binary patch delta 20 acmeBB?@;Gn&dbZi00g@Yw`}CD6$St^$ps+* delta 20 acmeBB?@;Gn&dbZi00i$3ZQjUTD+~ZQmIdVi diff --git a/ansible/lib/python3.11/site-packages/yaml/__pycache__/__init__.cpython-311.pyc b/ansible/lib/python3.11/site-packages/yaml/__pycache__/__init__.cpython-311.pyc index 44e3cf2bfab2857e6486a8edb3ac50c65defd336..c7ea409a9a138697d8df7ce59ac2a5652d014ba4 100644 GIT binary patch delta 22 ccmX@z#(27ok$X8WFBbz4>^9u8k=wx;08ZxyS^xk5 delta 22 ccmX@z#(27ok$X8WFBbz4{5rIGBe#Py08{}7DF6Tf diff --git a/ansible/lib/python3.11/site-packages/yaml/__pycache__/composer.cpython-311.pyc b/ansible/lib/python3.11/site-packages/yaml/__pycache__/composer.cpython-311.pyc index afe4650180007bf95a61d941a3ee5b697afcd19b..fe8fca08ee6f73394af389e1e9806e070021754f 100644 GIT binary patch delta 20 acmbPbKFgeYIWI340}$*s+_I6oUm5^7;RSyH delta 20 acmbPbKFgeYIWI340}%W=w0R?Uzcc_n`vy(` diff --git a/ansible/lib/python3.11/site-packages/yaml/__pycache__/constructor.cpython-311.pyc b/ansible/lib/python3.11/site-packages/yaml/__pycache__/constructor.cpython-311.pyc index b5dc80526b3bb77549fb43e07f3959a99302fc72..67e84977ec677762e892272907758d685cb6cb84 100644 GIT binary patch delta 22 ccmZo%$JDrviF-LOFBbz4>^9u8k^9>;080r6ApigX delta 22 ccmZo%$JDrviF-LOFBbz4{5rIGBlovy08k?b@Bjb+ diff --git a/ansible/lib/python3.11/site-packages/yaml/__pycache__/cyaml.cpython-311.pyc b/ansible/lib/python3.11/site-packages/yaml/__pycache__/cyaml.cpython-311.pyc index acf9f5c6557571cf0b3eade3c6c20fb57dfc1e4c..bbf89eca167adf5dfa849cfb4dc572ece4a18ac8 100644 GIT binary patch delta 20 acmbQKHB*awIWI340}$*s+_I6IPZR((-vrzM delta 20 acmbQKHB*awIWI340}%W=w0R>ppC|x2`30*0 diff --git a/ansible/lib/python3.11/site-packages/yaml/__pycache__/dumper.cpython-311.pyc b/ansible/lib/python3.11/site-packages/yaml/__pycache__/dumper.cpython-311.pyc index 0a42bf019ac78452cc4798962d3f3ea0a037ce8e..39253948764a9163e121d76c91f301130e56aed5 100644 GIT binary patch delta 20 acmdlYwndD4IWI340}$*s+_I5dlN$gxJp}sz delta 20 acmdlYwndD4IWI340}%W=w0R@9CN}^)R|U!d diff --git a/ansible/lib/python3.11/site-packages/yaml/__pycache__/emitter.cpython-311.pyc b/ansible/lib/python3.11/site-packages/yaml/__pycache__/emitter.cpython-311.pyc index 23e14aaa9469a2fb5bd660dc077f973e4ab0184e..90dff0857fb66d34731c22839563fd9ed54912c4 100644 GIT binary patch delta 36 pcmbQVka^NVX71&@yj%=Gu-kCUMs8MX#$THSt^1jQ)Iz(D69LFT40-?n delta 36 qcmbQVka^NVX71&@yj%=G@axd#johr(jJGxmTK6*psfBhQCjtQ0ObrPD diff --git a/ansible/lib/python3.11/site-packages/yaml/__pycache__/error.cpython-311.pyc b/ansible/lib/python3.11/site-packages/yaml/__pycache__/error.cpython-311.pyc index f7419e3c2f7df55c6fd21c3bf95e0227dd89b5e6..99c6e8d6a6e85d4b3fc23bc02db408d0a8f4e2e6 100644 GIT binary patch delta 20 acmZ3YxI~eAIWI340}$*s+_I5-rT_ppP6cKF delta 20 acmZ3YxI~eAIWI340}%W=w0R@^9u8k$a;%08YgQ!vFvP delta 22 ccmdnf&$y?bk$X8WFBbz4{5rIGBlkvk08`%wk^lez diff --git a/ansible/lib/python3.11/site-packages/yaml/__pycache__/resolver.cpython-311.pyc b/ansible/lib/python3.11/site-packages/yaml/__pycache__/resolver.cpython-311.pyc index fca28466a18d0587ce7092c88055eb1e018d14d3..cd681c04da96e9ac77b9e8cf3c0b11b10e1dba06 100644 GIT binary patch delta 20 acmdnsyTO-xIWI340}$*s+_I5-sTu%3bOrbT delta 20 acmdnsyTO-xIWI340}%W=w0R@=4`y>F>t_>sr diff --git a/ansible/lib/python3.11/site-packages/yaml/__pycache__/serializer.cpython-311.pyc b/ansible/lib/python3.11/site-packages/yaml/__pycache__/serializer.cpython-311.pyc index 15387a5d3728d1a43421e73ee750671ca4dba7d5..ff123d707d6287bbd6181d0a7db9b7b42146d26f 100644 GIT binary patch delta 20 acmdmMve$%rIWI340}$*s+_I6|ND2Tv>;+)} delta 20 acmdmMve$%rIWI340}%W=w0R@9krV(z1_m$y diff --git a/ansible/lib/python3.11/site-packages/yaml/__pycache__/tokens.cpython-311.pyc b/ansible/lib/python3.11/site-packages/yaml/__pycache__/tokens.cpython-311.pyc index 4cf53d50b34c80dcb5edbc1675736ac68db5bff4..25ab652c0e52ba8102068522e75cedda43cc7a75 100644 GIT binary patch delta 45 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 z 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 literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_context_deprecated.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_context_deprecated.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..05d420bda09d12e023593cb9c0262d77e09eafb1 GIT binary patch literal 34743 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%0D*hS(nA<}#e_ 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 literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_crypto_builtin_md4.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_crypto_builtin_md4.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..eb9d20d8a2f48e6b357bcf7f038fc9888077406b GIT binary patch literal 8270 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 F{{vxmORN9@ literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_crypto_digest.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_crypto_digest.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..045fb9e14c375a31eecb60089d6b06e09870359b GIT binary patch literal 21668 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 gkN3Hh=X~wG9l8wCVSDZkz!oeq@$2W)qmYybcN literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_crypto_scrypt.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_crypto_scrypt.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0a2290ac11b4e57d7030b77f87d43fdaa32c9b3c GIT binary patch literal 30351 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 literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_ext_django_source.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_ext_django_source.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6da1f05beafaa3507801fb49b28dbbdbe403779a GIT binary patch literal 9618 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 literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_handlers_argon2.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_handlers_argon2.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d8e1bafb5d24e5aaadc225a5f03f3ea2a586caf9 GIT binary patch literal 22292 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 literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_handlers_bcrypt.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_handlers_bcrypt.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d55995f2f307045d18131c15f1f97e428429d37b GIT binary patch literal 29991 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 literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_handlers_cisco.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_handlers_cisco.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dbbf28a84b964880eab26f4cd0aa911a5dcffb0c GIT binary patch literal 11703 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 literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_handlers_django.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_handlers_django.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8a4c68079a5176296bc266003c99d7da8dfcc7a2 GIT binary patch literal 18338 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 CkhQ-6 literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_handlers_scrypt.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_handlers_scrypt.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ab0a19767c9f09b043fb9cb70d8cee1f3492630e GIT binary patch literal 4159 zcmb_f&2Jjp6~BY|7!25P9Fvbmc_g8Fahw5Z&&Zr= zPDPR_di8NQf;sDJ!DNz71w$(lb@X&hucsE~^8Fq&btfPHg9d*@kaHkQAlP->P+8v- z%o;Gb7)s8_8VWkjgehy}O<5AbZDvh_&Vg4s@<0o+o`&8pOR{EOHz-X29~*yTIr6_hul!|23-ENDQ{^ZvWV1> z7{9s%*~Vg@I?9do&0&3hXG&ZM=YmdGZhI=WD^3lQU`6wW#{yJG!L+k{j7Ue3QH>~&ze+7 zM5TCw;hnrIQ}6`cJH=R$Sz4W9Sa#V@CDuYpF)40_{IzsSNi>Rc^Q&HIK=JH};pK8v zRKkqN27RGKupHuoflxB&d|b*ri4_w{HM~2!>Y5FBcm3-d%akt|o}oNt-nAVUUB#_T zHtJi&o~bZ-Vc-A?9pFQjdz^d4N_c8%DwQw!gzS8Mfsf7C(g|lFC`U5lZZcMzcjsfv ztM%pd!de3+YFqryvOAD0PsM9oD!IGN%BsFl69jaOttxQn{s^2Ra-~XRy$=Zr2a5)- zbKMizVm8uB!c&xU-lb|by6()+KMw3QU?A@peTsj;O zp0H8@0S(764l(DfbKXCV9v3D_i7nN{`J#Yw0n0hJLeX-d5NlM@n+u86NX4}so-e>K zw^ImghLwDnUEYao$Cg&obKyySBh_yy4nxXW-zgR5T)qNN&2IwS8z)Hq{<--aZPcAr=1 z_f2mC2%z*5GFm%Lz&QO5YD1M0pmaABhu9d1t(r8b2Dw({Ahe*FYLhu-y_#WQ1(g$7 zCF{M>NC8zA4)*5ws-gHVsAihZ3nlGL3Ob~5qd!|u5171x z_;LV||LudHN!y==m*GS5c8k3Ij&vQ7u0xV)kyMkUekL!q$ZrnF=KYmJaD|4{eFVp-w+3*_Y9P=koK^|mL?1B&xnIQRC{~?a1lIv-F3L>jjSa5$&qDc%)B1Yz-uW%S4IUIvaV z&ZfnAY`yfC$A5bKGIqcmTJN>2_nOvwXTb6vk>0lmxUjTmZ$qFjZ8j3U8-i;CT)T~v>z?-@8izvrJ`XY&fd^X3F1 zbhQle_jvjq_c8Dm9$^bn`(=fw?J89io~g<5B%wK1#yp4SPt}I`l1O)8Ydje6>^YmK z2c%h~M!ty%a`X7rcjL~ZapyteweEHB_4eU7)f%UoR!Z|-d{VEHG+kjtj;7UNnieIt zCg8D^rnhU1(BZ)QNZG%Dwxwg<*f|rR6UpT8$#=Dr_tEM2)7IJkE_}{ zA|fN8zFFkho)8WUEm|SQ7UJ_&TQ_OtIYFRlgoYO17>(a|q8sNz?PlOBBHIxMg>dh4 zJaYNRt^H{G3NSw*+D2f`5$zFR7PN5{)8UDxU22k5JiOk#AvZ|{%OQzJ-`q4d_1D@V z0+3_-doS#VrfV(JwKf2pslU;_sWYG*;0u)08-CLU7Z9u7fUr7GUM{z820mcMrwh2JJ35$ZmBT12EXwcD zja`R>aq^04qG?vjqv$dT63?TwqMCV5SwUk|Z*T~xLx=$<@D@9snz}`x6A9XUS%+wQh#-h#5NU!)dl(R-`y=?%w14}8Z`r?n zO&;2*mYsSrbPPuKwa+oQx=*&i)t6-RPWTW+TAjT{KwNKvYyIyr@Ha1dk3qP3(c88r z2?C*gF@5pd4;trFKVQ%R`<-_aGe;9MZ;OW$-?t{dZ-UFm6SvQ=AvwGM$nJl;cW4i` N?7=1&@4T9i{txrGNgn_J literal 0 HcmV?d00001 diff --git a/ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_hosts.cpython-311.pyc b/ansible/lib/python3.11/site-packages/passlib/tests/__pycache__/test_hosts.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4b195b85ec252168d070e8227cb923c4a77eb1d4 GIT binary patch literal 4581 zcmb7I+fN(W89(E(@t6#@frOAQbce24YLa4Xzyy@66fjA-1QH;ZwbELHXMBP2B{O3# zUTC9gA7~UQG-^c@sa>?H5>|Q1!>Zc8^lz9!Qw5D0X{D+UeRGvos(9*mjxX3;y6xfc zan5(n`OY`z{JzWKRYOBPg7Ulg*YOb-LVv}ba