"""passlib.ifc - abstract interfaces used by Passlib""" #============================================================================= # imports #============================================================================= # core import logging; log = logging.getLogger(__name__) import sys # site # pkg from passlib.utils.decor import deprecated_method # local __all__ = [ "PasswordHash", ] #============================================================================= # 2/3 compatibility helpers #============================================================================= def recreate_with_metaclass(meta): """class decorator that re-creates class using metaclass""" def builder(cls): if meta is type(cls): return cls return meta(cls.__name__, cls.__bases__, cls.__dict__.copy()) return builder #============================================================================= # PasswordHash interface #============================================================================= from abc import ABCMeta, abstractmethod, abstractproperty # TODO: make this actually use abstractproperty(), # now that we dropped py25, 'abc' is always available. # XXX: rename to PasswordHasher? @recreate_with_metaclass(ABCMeta) class PasswordHash(object): """This class describes an abstract interface which all password hashes in Passlib adhere to. Under Python 2.6 and up, this is an actual Abstract Base Class built using the :mod:`!abc` module. See the Passlib docs for full documentation. """ #=================================================================== # class attributes #=================================================================== #--------------------------------------------------------------- # general information #--------------------------------------------------------------- ##name ##setting_kwds ##context_kwds #: flag which indicates this hasher matches a "disabled" hash #: (e.g. unix_disabled, or django_disabled); and doesn't actually #: depend on the provided password. is_disabled = False #: Should be None, or a positive integer indicating hash #: doesn't support secrets larger than this value. #: Whether hash throws error or silently truncates secret #: depends on .truncate_error and .truncate_verify_reject flags below. #: NOTE: calls may treat as boolean, since value will never be 0. #: .. versionadded:: 1.7 #: .. TODO: passlib 1.8: deprecate/rename this attr to "max_secret_size"? truncate_size = None # NOTE: these next two default to the optimistic "ideal", # most hashes in passlib have to default to False # for backward compat and/or expected behavior with existing hashes. #: If True, .hash() should throw a :exc:`~passlib.exc.PasswordSizeError` for #: any secrets larger than .truncate_size. Many hashers default to False #: for historical / compatibility purposes, indicating they will silently #: truncate instead. All such hashers SHOULD support changing #: the policy via ``.using(truncate_error=True)``. #: .. versionadded:: 1.7 #: .. TODO: passlib 1.8: deprecate/rename this attr to "truncate_hash_error"? truncate_error = True #: If True, .verify() should reject secrets larger than max_password_size. #: Many hashers default to False for historical / compatibility purposes, #: indicating they will match on the truncated portion instead. #: .. versionadded:: 1.7.1 truncate_verify_reject = True #--------------------------------------------------------------- # salt information -- if 'salt' in setting_kwds #--------------------------------------------------------------- ##min_salt_size ##max_salt_size ##default_salt_size ##salt_chars ##default_salt_chars #--------------------------------------------------------------- # rounds information -- if 'rounds' in setting_kwds #--------------------------------------------------------------- ##min_rounds ##max_rounds ##default_rounds ##rounds_cost #--------------------------------------------------------------- # encoding info -- if 'encoding' in context_kwds #--------------------------------------------------------------- ##default_encoding #=================================================================== # primary methods #=================================================================== @classmethod @abstractmethod def hash(cls, secret, # * **setting_and_context_kwds): # pragma: no cover -- abstract method r""" Hash secret, returning result. Should handle generating salt, etc, and should return string containing identifier, salt & other configuration, as well as digest. :param \\*\\*settings_kwds: Pass in settings to customize configuration of resulting hash. .. deprecated:: 1.7 Starting with Passlib 1.7, callers should no longer pass settings keywords (e.g. ``rounds`` or ``salt`` directly to :meth:`!hash`); should use ``.using(**settings).hash(secret)`` construction instead. Support will be removed in Passlib 2.0. :param \\*\\*context_kwds: Specific algorithms may require context-specific information (such as the user login). """ # FIXME: need stub for classes that define .encrypt() instead ... # this should call .encrypt(), and check for recursion back to here. raise NotImplementedError("must be implemented by subclass") @deprecated_method(deprecated="1.7", removed="2.0", replacement=".hash()") @classmethod def encrypt(cls, *args, **kwds): """ Legacy alias for :meth:`hash`. .. deprecated:: 1.7 This method was renamed to :meth:`!hash` in version 1.7. This alias will be removed in version 2.0, and should only be used for compatibility with Passlib 1.3 - 1.6. """ return cls.hash(*args, **kwds) # XXX: could provide default implementation which hands value to # hash(), and then does constant-time comparision on the result # (after making both are same string type) @classmethod @abstractmethod def verify(cls, secret, hash, **context_kwds): # pragma: no cover -- abstract method """verify secret against hash, returns True/False""" raise NotImplementedError("must be implemented by subclass") #=================================================================== # configuration #=================================================================== @classmethod @abstractmethod def using(cls, relaxed=False, **kwds): """ Return another hasher object (typically a subclass of the current one), which integrates the configuration options specified by ``kwds``. This should *always* return a new object, even if no configuration options are changed. .. todo:: document which options are accepted. :returns: typically returns a subclass for most hasher implementations. .. todo:: add this method to main documentation. """ raise NotImplementedError("must be implemented by subclass") #=================================================================== # migration #=================================================================== @classmethod def needs_update(cls, hash, secret=None): """ check if hash's configuration is outside desired bounds, or contains some other internal option which requires updating the password hash. :param hash: hash string to examine :param secret: optional secret known to have verified against the provided hash. (this is used by some hashes to detect legacy algorithm mistakes). :return: whether secret needs re-hashing. .. versionadded:: 1.7 """ # by default, always report that we don't need update return False #=================================================================== # additional methods #=================================================================== @classmethod @abstractmethod def identify(cls, hash): # pragma: no cover -- abstract method """check if hash belongs to this scheme, returns True/False""" raise NotImplementedError("must be implemented by subclass") @deprecated_method(deprecated="1.7", removed="2.0") @classmethod def genconfig(cls, **setting_kwds): # pragma: no cover -- abstract method """ compile settings into a configuration string for genhash() .. deprecated:: 1.7 As of 1.7, this method is deprecated, and slated for complete removal in Passlib 2.0. For all known real-world uses, hashing a constant string should provide equivalent functionality. This deprecation may be reversed if a use-case presents itself in the mean time. """ # NOTE: this fallback runs full hash alg, w/ whatever cost param is passed along. # implementations (esp ones w/ variable cost) will want to subclass this # with a constant-time implementation that just renders a config string. if cls.context_kwds: raise NotImplementedError("must be implemented by subclass") return cls.using(**setting_kwds).hash("") @deprecated_method(deprecated="1.7", removed="2.0") @classmethod def genhash(cls, secret, config, **context): """ generated hash for secret, using settings from config/hash string .. deprecated:: 1.7 As of 1.7, this method is deprecated, and slated for complete removal in Passlib 2.0. This deprecation may be reversed if a use-case presents itself in the mean time. """ # XXX: if hashes reliably offered a .parse() method, could make a fallback for this. raise NotImplementedError("must be implemented by subclass") #=================================================================== # undocumented methods / attributes #=================================================================== # the following entry points are used internally by passlib, # and aren't documented as part of the exposed interface. # they are subject to change between releases, # but are documented here so there's a list of them *somewhere*. #--------------------------------------------------------------- # extra metdata #--------------------------------------------------------------- #: this attribute shouldn't be used by hashers themselves, #: it's reserved for the CryptContext to track which hashers are deprecated. #: Note the context will only set this on objects it owns (and generated by .using()), #: and WONT set it on global objects. #: [added in 1.7] #: TODO: document this, or at least the use of testing for #: 'CryptContext().handler().deprecated' deprecated = False #: optionally present if hasher corresponds to format built into Django. #: this attribute (if not None) should be the Django 'algorithm' name. #: also indicates to passlib.ext.django that (when installed in django), #: django's native hasher should be used in preference to this one. ## django_name #--------------------------------------------------------------- # checksum information - defined for many hashes #--------------------------------------------------------------- ## checksum_chars ## checksum_size #--------------------------------------------------------------- # experimental methods #--------------------------------------------------------------- ##@classmethod ##def normhash(cls, hash): ## """helper to clean up non-canonic instances of hash. ## currently only provided by bcrypt() to fix an historical passlib issue. ## """ # experimental helper to parse hash into components. ##@classmethod ##def parsehash(cls, hash, checksum=True, sanitize=False): ## """helper to parse hash into components, returns dict""" # experiment helper to estimate bitsize of different hashes, # implement for GenericHandler, but may be currently be off for some hashes. # want to expand this into a way to programmatically compare # "strengths" of different hashes and hash algorithms. # still needs to have some factor for estimate relative cost per round, # ala in the style of the scrypt whitepaper. ##@classmethod ##def bitsize(cls, **kwds): ## """returns dict mapping component -> bits contributed. ## components currently include checksum, salt, rounds. ## """ #=================================================================== # eoc #=================================================================== class DisabledHash(PasswordHash): """ extended disabled-hash methods; only need be present if .disabled = True """ is_disabled = True @classmethod def disable(cls, hash=None): """ return string representing a 'disabled' hash; optionally including previously enabled hash (this is up to the individual scheme). """ # default behavior: ignore original hash, return standalone marker return cls.hash("") @classmethod def enable(cls, hash): """ given a disabled-hash string, extract previously-enabled hash if one is present, otherwise raises ValueError """ # default behavior: no way to restore original hash raise ValueError("cannot restore original hash") #============================================================================= # eof #=============================================================================