You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					215 lines
				
				7.6 KiB
			
		
		
			
		
	
	
					215 lines
				
				7.6 KiB
			| 
								 
											3 years ago
										 
									 | 
							
								"""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
							 | 
						||
| 
								 | 
							
								#=============================================================================
							 |