diff --git a/venv/Lib/site-packages/passlib/handlers/__init__.py b/venv/Lib/site-packages/passlib/handlers/__init__.py new file mode 100644 index 0000000..0a0338c --- /dev/null +++ b/venv/Lib/site-packages/passlib/handlers/__init__.py @@ -0,0 +1 @@ +"""passlib.handlers -- holds implementations of all passlib's builtin hash formats""" diff --git a/venv/Lib/site-packages/passlib/handlers/argon2.py b/venv/Lib/site-packages/passlib/handlers/argon2.py new file mode 100644 index 0000000..4a5691b --- /dev/null +++ b/venv/Lib/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/venv/Lib/site-packages/passlib/handlers/bcrypt.py b/venv/Lib/site-packages/passlib/handlers/bcrypt.py new file mode 100644 index 0000000..b83b110 --- /dev/null +++ b/venv/Lib/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/venv/Lib/site-packages/passlib/handlers/cisco.py b/venv/Lib/site-packages/passlib/handlers/cisco.py new file mode 100644 index 0000000..e715e1a --- /dev/null +++ b/venv/Lib/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/venv/Lib/site-packages/passlib/handlers/des_crypt.py b/venv/Lib/site-packages/passlib/handlers/des_crypt.py new file mode 100644 index 0000000..68a4ca7 --- /dev/null +++ b/venv/Lib/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 +#=============================================================================