view MoinMoin/support/passlib/handlers/fshp.py @ 6130:7f0616feeae9

upgrade passlib to 1.7.1, update CHANGES
author Thomas Waldmann <tw AT waldmann-edv DOT de>
date Tue, 11 Apr 2017 22:53:58 +0200
parents 86a41c2bedec
children
line wrap: on
line source
"""passlib.handlers.fshp
"""

#=============================================================================
# imports
#=============================================================================
# core
from base64 import b64encode, b64decode
import re
import logging; log = logging.getLogger(__name__)
# site
# pkg
from passlib.utils import to_unicode
import passlib.utils.handlers as uh
from passlib.utils.compat import bascii_to_str, iteritems, u,\
                                 unicode
from passlib.crypto.digest import pbkdf1
# local
__all__ = [
    'fshp',
]
#=============================================================================
# sha1-crypt
#=============================================================================
class fshp(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
    """This class implements the FSHP password hash, and follows the :ref:`password-hash-api`.

    It supports a variable-length salt, and a variable number of rounds.

    The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:

    :param salt:
        Optional raw salt string.
        If not specified, one will be autogenerated (this is recommended).

    :param salt_size:
        Optional number of bytes to use when autogenerating new salts.
        Defaults to 16 bytes, but can be any non-negative value.

    :param rounds:
        Optional number of rounds to use.
        Defaults to 480000, must be between 1 and 4294967295, inclusive.

    :param variant:
        Optionally specifies variant of FSHP to use.

        * ``0`` - uses SHA-1 digest (deprecated).
        * ``1`` - uses SHA-2/256 digest (default).
        * ``2`` - uses SHA-2/384 digest.
        * ``3`` - uses SHA-2/512 digest.

    :type relaxed: bool
    :param relaxed:
        By default, providing an invalid value for one of the other
        keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
        and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
        will be issued instead. Correctable errors include ``rounds``
        that are too small or too large, and ``salt`` strings that are too long.

        .. versionadded:: 1.6
    """

    #===================================================================
    # class attrs
    #===================================================================
    #--GenericHandler--
    name = "fshp"
    setting_kwds = ("salt", "salt_size", "rounds", "variant")
    checksum_chars = uh.PADDED_BASE64_CHARS
    ident = u("{FSHP")
    # checksum_size is property() that depends on variant

    #--HasRawSalt--
    default_salt_size = 16 # current passlib default, FSHP uses 8
    max_salt_size = None

    #--HasRounds--
    # FIXME: should probably use different default rounds
    # based on the variant. setting for default variant (sha256) for now.
    default_rounds = 480000 # current passlib default, FSHP uses 4096
    min_rounds = 1 # set by FSHP
    max_rounds = 4294967295 # 32-bit integer limit - not set by FSHP
    rounds_cost = "linear"

    #--variants--
    default_variant = 1
    _variant_info = {
        # variant: (hash name, digest size)
        0: ("sha1",     20),
        1: ("sha256",   32),
        2: ("sha384",   48),
        3: ("sha512",   64),
        }
    _variant_aliases = dict(
        [(unicode(k),k) for k in _variant_info] +
        [(v[0],k) for k,v in iteritems(_variant_info)]
        )

    #===================================================================
    # configuration
    #===================================================================
    @classmethod
    def using(cls, variant=None, **kwds):
        subcls = super(fshp, cls).using(**kwds)
        if variant is not None:
            subcls.default_variant = cls._norm_variant(variant)
        return subcls

    #===================================================================
    # instance attrs
    #===================================================================
    variant = None

    #===================================================================
    # init
    #===================================================================
    def __init__(self, variant=None, **kwds):
        # NOTE: variant must be set first, since it controls checksum size, etc.
        self.use_defaults = kwds.get("use_defaults") # load this early
        if variant is not None:
            variant = self._norm_variant(variant)
        elif self.use_defaults:
            variant = self.default_variant
            assert self._norm_variant(variant) == variant, "invalid default variant: %r" % (variant,)
        else:
            raise TypeError("no variant specified")
        self.variant = variant
        super(fshp, self).__init__(**kwds)

    @classmethod
    def _norm_variant(cls, variant):
        if isinstance(variant, bytes):
            variant = variant.decode("ascii")
        if isinstance(variant, unicode):
            try:
                variant = cls._variant_aliases[variant]
            except KeyError:
                raise ValueError("invalid fshp variant")
        if not isinstance(variant, int):
            raise TypeError("fshp variant must be int or known alias")
        if variant not in cls._variant_info:
            raise ValueError("invalid fshp variant")
        return variant

    @property
    def checksum_alg(self):
        return self._variant_info[self.variant][0]

    @property
    def checksum_size(self):
        return self._variant_info[self.variant][1]

    #===================================================================
    # formatting
    #===================================================================

    _hash_regex = re.compile(u(r"""
            ^
            \{FSHP
            (\d+)\| # variant
            (\d+)\| # salt size
            (\d+)\} # rounds
            ([a-zA-Z0-9+/]+={0,3}) # digest
            $"""), re.X)

    @classmethod
    def from_string(cls, hash):
        hash = to_unicode(hash, "ascii", "hash")
        m = cls._hash_regex.match(hash)
        if not m:
            raise uh.exc.InvalidHashError(cls)
        variant, salt_size, rounds, data = m.group(1,2,3,4)
        variant = int(variant)
        salt_size = int(salt_size)
        rounds = int(rounds)
        try:
            data = b64decode(data.encode("ascii"))
        except TypeError:
            raise uh.exc.MalformedHashError(cls)
        salt = data[:salt_size]
        chk = data[salt_size:]
        return cls(salt=salt, checksum=chk, rounds=rounds, variant=variant)

    def to_string(self):
        chk = self.checksum
        salt = self.salt
        data = bascii_to_str(b64encode(salt+chk))
        return "{FSHP%d|%d|%d}%s" % (self.variant, len(salt), self.rounds, data)

    #===================================================================
    # backend
    #===================================================================

    def _calc_checksum(self, secret):
        if isinstance(secret, unicode):
            secret = secret.encode("utf-8")
        # NOTE: for some reason, FSHP uses pbkdf1 with password & salt reversed.
        #       this has only a minimal impact on security,
        #       but it is worth noting this deviation.
        return pbkdf1(
            digest=self.checksum_alg,
            secret=self.salt,
            salt=secret,
            rounds=self.rounds,
            keylen=self.checksum_size,
            )

    #===================================================================
    # eoc
    #===================================================================

#=============================================================================
# eof
#=============================================================================