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.
		
		
		
		
		
			
		
			
				
					
					
						
							583 lines
						
					
					
						
							22 KiB
						
					
					
				
			
		
		
	
	
							583 lines
						
					
					
						
							22 KiB
						
					
					
				"""passlib.handlers.scram - hash for SCRAM credential storage"""
 | 
						|
#=============================================================================
 | 
						|
# imports
 | 
						|
#=============================================================================
 | 
						|
# core
 | 
						|
import logging; log = logging.getLogger(__name__)
 | 
						|
# site
 | 
						|
# pkg
 | 
						|
from passlib.utils import consteq, saslprep, to_native_str, splitcomma
 | 
						|
from passlib.utils.binary import ab64_decode, ab64_encode
 | 
						|
from passlib.utils.compat import bascii_to_str, iteritems, u, native_string_types
 | 
						|
from passlib.crypto.digest import pbkdf2_hmac, norm_hash_name
 | 
						|
import passlib.utils.handlers as uh
 | 
						|
# local
 | 
						|
__all__ = [
 | 
						|
    "scram",
 | 
						|
]
 | 
						|
 | 
						|
#=============================================================================
 | 
						|
# scram credentials hash
 | 
						|
#=============================================================================
 | 
						|
class scram(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
 | 
						|
    """This class provides a format for storing SCRAM passwords, and follows
 | 
						|
    the :ref:`password-hash-api`.
 | 
						|
 | 
						|
    It supports a variable-length salt, and a variable number of rounds.
 | 
						|
 | 
						|
    The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
 | 
						|
 | 
						|
    :type salt: bytes
 | 
						|
    :param salt:
 | 
						|
        Optional salt bytes.
 | 
						|
        If specified, the length must be between 0-1024 bytes.
 | 
						|
        If not specified, a 12 byte salt will be autogenerated
 | 
						|
        (this is recommended).
 | 
						|
 | 
						|
    :type salt_size: int
 | 
						|
    :param salt_size:
 | 
						|
        Optional number of bytes to use when autogenerating new salts.
 | 
						|
        Defaults to 12 bytes, but can be any value between 0 and 1024.
 | 
						|
 | 
						|
    :type rounds: int
 | 
						|
    :param rounds:
 | 
						|
        Optional number of rounds to use.
 | 
						|
        Defaults to 100000, but must be within ``range(1,1<<32)``.
 | 
						|
 | 
						|
    :type algs: list of strings
 | 
						|
    :param algs:
 | 
						|
        Specify list of digest algorithms to use.
 | 
						|
 | 
						|
        By default each scram hash will contain digests for SHA-1,
 | 
						|
        SHA-256, and SHA-512. This can be overridden by specify either be a
 | 
						|
        list such as ``["sha-1", "sha-256"]``, or a comma-separated string
 | 
						|
        such as ``"sha-1, sha-256"``. Names are case insensitive, and may
 | 
						|
        use :mod:`!hashlib` or `IANA <http://www.iana.org/assignments/hash-function-text-names>`_
 | 
						|
        hash names.
 | 
						|
 | 
						|
    :type relaxed: bool
 | 
						|
    :param relaxed:
 | 
						|
        By default, providing an invalid value for one of the other
 | 
						|
        keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
 | 
						|
        and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
 | 
						|
        will be issued instead. Correctable errors include ``rounds``
 | 
						|
        that are too small or too large, and ``salt`` strings that are too long.
 | 
						|
 | 
						|
        .. versionadded:: 1.6
 | 
						|
 | 
						|
    In addition to the standard :ref:`password-hash-api` methods,
 | 
						|
    this class also provides the following methods for manipulating Passlib
 | 
						|
    scram hashes in ways useful for pluging into a SCRAM protocol stack:
 | 
						|
 | 
						|
    .. automethod:: extract_digest_info
 | 
						|
    .. automethod:: extract_digest_algs
 | 
						|
    .. automethod:: derive_digest
 | 
						|
    """
 | 
						|
    #===================================================================
 | 
						|
    # class attrs
 | 
						|
    #===================================================================
 | 
						|
 | 
						|
    # NOTE: unlike most GenericHandler classes, the 'checksum' attr of
 | 
						|
    # ScramHandler is actually a map from digest_name -> digest, so
 | 
						|
    # many of the standard methods have been overridden.
 | 
						|
 | 
						|
    # NOTE: max_salt_size and max_rounds are arbitrarily chosen to provide
 | 
						|
    # a sanity check; the underlying pbkdf2 specifies no bounds for either.
 | 
						|
 | 
						|
    #--GenericHandler--
 | 
						|
    name = "scram"
 | 
						|
    setting_kwds = ("salt", "salt_size", "rounds", "algs")
 | 
						|
    ident = u("$scram$")
 | 
						|
 | 
						|
    #--HasSalt--
 | 
						|
    default_salt_size = 12
 | 
						|
    max_salt_size = 1024
 | 
						|
 | 
						|
    #--HasRounds--
 | 
						|
    default_rounds = 100000
 | 
						|
    min_rounds = 1
 | 
						|
    max_rounds = 2**32-1
 | 
						|
    rounds_cost = "linear"
 | 
						|
 | 
						|
    #--custom--
 | 
						|
 | 
						|
    # default algorithms when creating new hashes.
 | 
						|
    default_algs = ["sha-1", "sha-256", "sha-512"]
 | 
						|
 | 
						|
    # list of algs verify prefers to use, in order.
 | 
						|
    _verify_algs = ["sha-256", "sha-512", "sha-224", "sha-384", "sha-1"]
 | 
						|
 | 
						|
    #===================================================================
 | 
						|
    # instance attrs
 | 
						|
    #===================================================================
 | 
						|
 | 
						|
    # 'checksum' is different from most GenericHandler subclasses,
 | 
						|
    # in that it contains a dict mapping from alg -> digest,
 | 
						|
    # or None if no checksum present.
 | 
						|
 | 
						|
    # list of algorithms to create/compare digests for.
 | 
						|
    algs = None
 | 
						|
 | 
						|
    #===================================================================
 | 
						|
    # scram frontend helpers
 | 
						|
    #===================================================================
 | 
						|
    @classmethod
 | 
						|
    def extract_digest_info(cls, hash, alg):
 | 
						|
        """return (salt, rounds, digest) for specific hash algorithm.
 | 
						|
 | 
						|
        :type hash: str
 | 
						|
        :arg hash:
 | 
						|
            :class:`!scram` hash stored for desired user
 | 
						|
 | 
						|
        :type alg: str
 | 
						|
        :arg alg:
 | 
						|
            Name of digest algorithm (e.g. ``"sha-1"``) requested by client.
 | 
						|
 | 
						|
            This value is run through :func:`~passlib.crypto.digest.norm_hash_name`,
 | 
						|
            so it is case-insensitive, and can be the raw SCRAM
 | 
						|
            mechanism name (e.g. ``"SCRAM-SHA-1"``), the IANA name,
 | 
						|
            or the hashlib name.
 | 
						|
 | 
						|
        :raises KeyError:
 | 
						|
            If the hash does not contain an entry for the requested digest
 | 
						|
            algorithm.
 | 
						|
 | 
						|
        :returns:
 | 
						|
            A tuple containing ``(salt, rounds, digest)``,
 | 
						|
            where *digest* matches the raw bytes returned by
 | 
						|
            SCRAM's :func:`Hi` function for the stored password,
 | 
						|
            the provided *salt*, and the iteration count (*rounds*).
 | 
						|
            *salt* and *digest* are both raw (unencoded) bytes.
 | 
						|
        """
 | 
						|
        # XXX: this could be sped up by writing custom parsing routine
 | 
						|
        # that just picks out relevant digest, and doesn't bother
 | 
						|
        # with full structure validation each time it's called.
 | 
						|
        alg = norm_hash_name(alg, 'iana')
 | 
						|
        self = cls.from_string(hash)
 | 
						|
        chkmap = self.checksum
 | 
						|
        if not chkmap:
 | 
						|
            raise ValueError("scram hash contains no digests")
 | 
						|
        return self.salt, self.rounds, chkmap[alg]
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def extract_digest_algs(cls, hash, format="iana"):
 | 
						|
        """Return names of all algorithms stored in a given hash.
 | 
						|
 | 
						|
        :type hash: str
 | 
						|
        :arg hash:
 | 
						|
            The :class:`!scram` hash to parse
 | 
						|
 | 
						|
        :type format: str
 | 
						|
        :param format:
 | 
						|
            This changes the naming convention used by the
 | 
						|
            returned algorithm names. By default the names
 | 
						|
            are IANA-compatible; possible values are ``"iana"`` or ``"hashlib"``.
 | 
						|
 | 
						|
        :returns:
 | 
						|
            Returns a list of digest algorithms; e.g. ``["sha-1"]``
 | 
						|
        """
 | 
						|
        # XXX: this could be sped up by writing custom parsing routine
 | 
						|
        # that just picks out relevant names, and doesn't bother
 | 
						|
        # with full structure validation each time it's called.
 | 
						|
        algs = cls.from_string(hash).algs
 | 
						|
        if format == "iana":
 | 
						|
            return algs
 | 
						|
        else:
 | 
						|
            return [norm_hash_name(alg, format) for alg in algs]
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def derive_digest(cls, password, salt, rounds, alg):
 | 
						|
        """helper to create SaltedPassword digest for SCRAM.
 | 
						|
 | 
						|
        This performs the step in the SCRAM protocol described as::
 | 
						|
 | 
						|
            SaltedPassword  := Hi(Normalize(password), salt, i)
 | 
						|
 | 
						|
        :type password: unicode or utf-8 bytes
 | 
						|
        :arg password: password to run through digest
 | 
						|
 | 
						|
        :type salt: bytes
 | 
						|
        :arg salt: raw salt data
 | 
						|
 | 
						|
        :type rounds: int
 | 
						|
        :arg rounds: number of iterations.
 | 
						|
 | 
						|
        :type alg: str
 | 
						|
        :arg alg: name of digest to use (e.g. ``"sha-1"``).
 | 
						|
 | 
						|
        :returns:
 | 
						|
            raw bytes of ``SaltedPassword``
 | 
						|
        """
 | 
						|
        if isinstance(password, bytes):
 | 
						|
            password = password.decode("utf-8")
 | 
						|
        # NOTE: pbkdf2_hmac() will encode secret & salt using utf-8,
 | 
						|
        #       and handle normalizing alg name.
 | 
						|
        return pbkdf2_hmac(alg, saslprep(password), salt, rounds)
 | 
						|
 | 
						|
    #===================================================================
 | 
						|
    # serialization
 | 
						|
    #===================================================================
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def from_string(cls, hash):
 | 
						|
        hash = to_native_str(hash, "ascii", "hash")
 | 
						|
        if not hash.startswith("$scram$"):
 | 
						|
            raise uh.exc.InvalidHashError(cls)
 | 
						|
        parts = hash[7:].split("$")
 | 
						|
        if len(parts) != 3:
 | 
						|
            raise uh.exc.MalformedHashError(cls)
 | 
						|
        rounds_str, salt_str, chk_str = parts
 | 
						|
 | 
						|
        # decode rounds
 | 
						|
        rounds = int(rounds_str)
 | 
						|
        if rounds_str != str(rounds): # forbid zero padding, etc.
 | 
						|
            raise uh.exc.MalformedHashError(cls)
 | 
						|
 | 
						|
        # decode salt
 | 
						|
        try:
 | 
						|
            salt = ab64_decode(salt_str.encode("ascii"))
 | 
						|
        except TypeError:
 | 
						|
            raise uh.exc.MalformedHashError(cls)
 | 
						|
 | 
						|
        # decode algs/digest list
 | 
						|
        if not chk_str:
 | 
						|
            # scram hashes MUST have something here.
 | 
						|
            raise uh.exc.MalformedHashError(cls)
 | 
						|
        elif "=" in chk_str:
 | 
						|
            # comma-separated list of 'alg=digest' pairs
 | 
						|
            algs = None
 | 
						|
            chkmap = {}
 | 
						|
            for pair in chk_str.split(","):
 | 
						|
                alg, digest = pair.split("=")
 | 
						|
                try:
 | 
						|
                    chkmap[alg] = ab64_decode(digest.encode("ascii"))
 | 
						|
                except TypeError:
 | 
						|
                    raise uh.exc.MalformedHashError(cls)
 | 
						|
        else:
 | 
						|
            # comma-separated list of alg names, no digests
 | 
						|
            algs = chk_str
 | 
						|
            chkmap = None
 | 
						|
 | 
						|
        # return new object
 | 
						|
        return cls(
 | 
						|
            rounds=rounds,
 | 
						|
            salt=salt,
 | 
						|
            checksum=chkmap,
 | 
						|
            algs=algs,
 | 
						|
        )
 | 
						|
 | 
						|
    def to_string(self):
 | 
						|
        salt = bascii_to_str(ab64_encode(self.salt))
 | 
						|
        chkmap = self.checksum
 | 
						|
        chk_str = ",".join(
 | 
						|
            "%s=%s" % (alg, bascii_to_str(ab64_encode(chkmap[alg])))
 | 
						|
            for alg in self.algs
 | 
						|
        )
 | 
						|
        return '$scram$%d$%s$%s' % (self.rounds, salt, chk_str)
 | 
						|
 | 
						|
    #===================================================================
 | 
						|
    # variant constructor
 | 
						|
    #===================================================================
 | 
						|
    @classmethod
 | 
						|
    def using(cls, default_algs=None, algs=None, **kwds):
 | 
						|
        # parse aliases
 | 
						|
        if algs is not None:
 | 
						|
            assert default_algs is None
 | 
						|
            default_algs = algs
 | 
						|
 | 
						|
        # create subclass
 | 
						|
        subcls = super(scram, cls).using(**kwds)
 | 
						|
 | 
						|
        # fill in algs
 | 
						|
        if default_algs is not None:
 | 
						|
            subcls.default_algs = cls._norm_algs(default_algs)
 | 
						|
        return subcls
 | 
						|
 | 
						|
    #===================================================================
 | 
						|
    # init
 | 
						|
    #===================================================================
 | 
						|
    def __init__(self, algs=None, **kwds):
 | 
						|
        super(scram, self).__init__(**kwds)
 | 
						|
 | 
						|
        # init algs
 | 
						|
        digest_map = self.checksum
 | 
						|
        if algs is not None:
 | 
						|
            if digest_map is not None:
 | 
						|
                raise RuntimeError("checksum & algs kwds are mutually exclusive")
 | 
						|
            algs = self._norm_algs(algs)
 | 
						|
        elif digest_map is not None:
 | 
						|
            # derive algs list from digest map (if present).
 | 
						|
            algs = self._norm_algs(digest_map.keys())
 | 
						|
        elif self.use_defaults:
 | 
						|
            algs = list(self.default_algs)
 | 
						|
            assert self._norm_algs(algs) == algs, "invalid default algs: %r" % (algs,)
 | 
						|
        else:
 | 
						|
            raise TypeError("no algs list specified")
 | 
						|
        self.algs = algs
 | 
						|
 | 
						|
    def _norm_checksum(self, checksum, relaxed=False):
 | 
						|
        if not isinstance(checksum, dict):
 | 
						|
            raise uh.exc.ExpectedTypeError(checksum, "dict", "checksum")
 | 
						|
        for alg, digest in iteritems(checksum):
 | 
						|
            if alg != norm_hash_name(alg, 'iana'):
 | 
						|
                raise ValueError("malformed algorithm name in scram hash: %r" %
 | 
						|
                                 (alg,))
 | 
						|
            if len(alg) > 9:
 | 
						|
                raise ValueError("SCRAM limits algorithm names to "
 | 
						|
                                 "9 characters: %r" % (alg,))
 | 
						|
            if not isinstance(digest, bytes):
 | 
						|
                raise uh.exc.ExpectedTypeError(digest, "raw bytes", "digests")
 | 
						|
            # TODO: verify digest size (if digest is known)
 | 
						|
        if 'sha-1' not in checksum:
 | 
						|
            # NOTE: required because of SCRAM spec.
 | 
						|
            raise ValueError("sha-1 must be in algorithm list of scram hash")
 | 
						|
        return checksum
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def _norm_algs(cls, algs):
 | 
						|
        """normalize algs parameter"""
 | 
						|
        if isinstance(algs, native_string_types):
 | 
						|
            algs = splitcomma(algs)
 | 
						|
        algs = sorted(norm_hash_name(alg, 'iana') for alg in algs)
 | 
						|
        if any(len(alg)>9 for alg in algs):
 | 
						|
            raise ValueError("SCRAM limits alg names to max of 9 characters")
 | 
						|
        if 'sha-1' not in algs:
 | 
						|
            # NOTE: required because of SCRAM spec (rfc 5802)
 | 
						|
            raise ValueError("sha-1 must be in algorithm list of scram hash")
 | 
						|
        return algs
 | 
						|
 | 
						|
    #===================================================================
 | 
						|
    # migration
 | 
						|
    #===================================================================
 | 
						|
    def _calc_needs_update(self, **kwds):
 | 
						|
        # marks hashes as deprecated if they don't include at least all default_algs.
 | 
						|
        # XXX: should we deprecate if they aren't exactly the same,
 | 
						|
        #      to permit removing legacy hashes?
 | 
						|
        if not set(self.algs).issuperset(self.default_algs):
 | 
						|
            return True
 | 
						|
 | 
						|
        # hand off to base implementation
 | 
						|
        return super(scram, self)._calc_needs_update(**kwds)
 | 
						|
 | 
						|
    #===================================================================
 | 
						|
    # digest methods
 | 
						|
    #===================================================================
 | 
						|
    def _calc_checksum(self, secret, alg=None):
 | 
						|
        rounds = self.rounds
 | 
						|
        salt = self.salt
 | 
						|
        hash = self.derive_digest
 | 
						|
        if alg:
 | 
						|
            # if requested, generate digest for specific alg
 | 
						|
            return hash(secret, salt, rounds, alg)
 | 
						|
        else:
 | 
						|
            # by default, return dict containing digests for all algs
 | 
						|
            return dict(
 | 
						|
                (alg, hash(secret, salt, rounds, alg))
 | 
						|
                for alg in self.algs
 | 
						|
            )
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def verify(cls, secret, hash, full=False):
 | 
						|
        uh.validate_secret(secret)
 | 
						|
        self = cls.from_string(hash)
 | 
						|
        chkmap = self.checksum
 | 
						|
        if not chkmap:
 | 
						|
            raise ValueError("expected %s hash, got %s config string instead" %
 | 
						|
                             (cls.name, cls.name))
 | 
						|
 | 
						|
        # NOTE: to make the verify method efficient, we just calculate hash
 | 
						|
        # of shortest digest by default. apps can pass in "full=True" to
 | 
						|
        # check entire hash for consistency.
 | 
						|
        if full:
 | 
						|
            correct = failed = False
 | 
						|
            for alg, digest in iteritems(chkmap):
 | 
						|
                other = self._calc_checksum(secret, alg)
 | 
						|
                # NOTE: could do this length check in norm_algs(),
 | 
						|
                # but don't need to be that strict, and want to be able
 | 
						|
                # to parse hashes containing algs not supported by platform.
 | 
						|
                # it's fine if we fail here though.
 | 
						|
                if len(digest) != len(other):
 | 
						|
                    raise ValueError("mis-sized %s digest in scram hash: %r != %r"
 | 
						|
                                     % (alg, len(digest), len(other)))
 | 
						|
                if consteq(other, digest):
 | 
						|
                    correct = True
 | 
						|
                else:
 | 
						|
                    failed = True
 | 
						|
            if correct and failed:
 | 
						|
                raise ValueError("scram hash verified inconsistently, "
 | 
						|
                                 "may be corrupted")
 | 
						|
            else:
 | 
						|
                return correct
 | 
						|
        else:
 | 
						|
            # XXX: should this just always use sha1 hash? would be faster.
 | 
						|
            # otherwise only verify against one hash, pick one w/ best security.
 | 
						|
            for alg in self._verify_algs:
 | 
						|
                if alg in chkmap:
 | 
						|
                    other = self._calc_checksum(secret, alg)
 | 
						|
                    return consteq(other, chkmap[alg])
 | 
						|
            # there should always be sha-1 at the very least,
 | 
						|
            # or something went wrong inside _norm_algs()
 | 
						|
            raise AssertionError("sha-1 digest not found!")
 | 
						|
 | 
						|
    #===================================================================
 | 
						|
    #
 | 
						|
    #===================================================================
 | 
						|
 | 
						|
#=============================================================================
 | 
						|
# code used for testing scram against protocol examples during development.
 | 
						|
#=============================================================================
 | 
						|
##def _test_reference_scram():
 | 
						|
##    "quick hack testing scram reference vectors"
 | 
						|
##    # NOTE: "n,," is GS2 header - see https://tools.ietf.org/html/rfc5801
 | 
						|
##    from passlib.utils.compat import print_
 | 
						|
##
 | 
						|
##    engine = _scram_engine(
 | 
						|
##        alg="sha-1",
 | 
						|
##        salt='QSXCR+Q6sek8bf92'.decode("base64"),
 | 
						|
##        rounds=4096,
 | 
						|
##        password=u("pencil"),
 | 
						|
##    )
 | 
						|
##    print_(engine.digest.encode("base64").rstrip())
 | 
						|
##
 | 
						|
##    msg = engine.format_auth_msg(
 | 
						|
##        username="user",
 | 
						|
##        client_nonce = "fyko+d2lbbFgONRv9qkxdawL",
 | 
						|
##        server_nonce = "3rfcNHYJY1ZVvWVs7j",
 | 
						|
##        header='c=biws',
 | 
						|
##    )
 | 
						|
##
 | 
						|
##    cp = engine.get_encoded_client_proof(msg)
 | 
						|
##    assert cp == "v0X8v3Bz2T0CJGbJQyF0X+HI4Ts=", cp
 | 
						|
##
 | 
						|
##    ss = engine.get_encoded_server_sig(msg)
 | 
						|
##    assert ss == "rmF9pqV8S7suAoZWja4dJRkFsKQ=", ss
 | 
						|
##
 | 
						|
##class _scram_engine(object):
 | 
						|
##    """helper class for verifying scram hash behavior
 | 
						|
##    against SCRAM protocol examples. not officially part of Passlib.
 | 
						|
##
 | 
						|
##    takes in alg, salt, rounds, and a digest or password.
 | 
						|
##
 | 
						|
##    can calculate the various keys & messages of the scram protocol.
 | 
						|
##
 | 
						|
##    """
 | 
						|
##    #=========================================================
 | 
						|
##    # init
 | 
						|
##    #=========================================================
 | 
						|
##
 | 
						|
##    @classmethod
 | 
						|
##    def from_string(cls, hash, alg):
 | 
						|
##        "create record from scram hash, for given alg"
 | 
						|
##        return cls(alg, *scram.extract_digest_info(hash, alg))
 | 
						|
##
 | 
						|
##    def __init__(self, alg, salt, rounds, digest=None, password=None):
 | 
						|
##        self.alg = norm_hash_name(alg)
 | 
						|
##        self.salt = salt
 | 
						|
##        self.rounds = rounds
 | 
						|
##        self.password = password
 | 
						|
##        if password:
 | 
						|
##            data = scram.derive_digest(password, salt, rounds, alg)
 | 
						|
##            if digest and data != digest:
 | 
						|
##                raise ValueError("password doesn't match digest")
 | 
						|
##            else:
 | 
						|
##                digest = data
 | 
						|
##        elif not digest:
 | 
						|
##            raise TypeError("must provide password or digest")
 | 
						|
##        self.digest = digest
 | 
						|
##
 | 
						|
##    #=========================================================
 | 
						|
##    # frontend methods
 | 
						|
##    #=========================================================
 | 
						|
##    def get_hash(self, data):
 | 
						|
##        "return hash of raw data"
 | 
						|
##        return hashlib.new(iana_to_hashlib(self.alg), data).digest()
 | 
						|
##
 | 
						|
##    def get_client_proof(self, msg):
 | 
						|
##        "return client proof of specified auth msg text"
 | 
						|
##        return xor_bytes(self.client_key, self.get_client_sig(msg))
 | 
						|
##
 | 
						|
##    def get_encoded_client_proof(self, msg):
 | 
						|
##        return self.get_client_proof(msg).encode("base64").rstrip()
 | 
						|
##
 | 
						|
##    def get_client_sig(self, msg):
 | 
						|
##        "return client signature of specified auth msg text"
 | 
						|
##        return self.get_hmac(self.stored_key, msg)
 | 
						|
##
 | 
						|
##    def get_server_sig(self, msg):
 | 
						|
##        "return server signature of specified auth msg text"
 | 
						|
##        return self.get_hmac(self.server_key, msg)
 | 
						|
##
 | 
						|
##    def get_encoded_server_sig(self, msg):
 | 
						|
##        return self.get_server_sig(msg).encode("base64").rstrip()
 | 
						|
##
 | 
						|
##    def format_server_response(self, client_nonce, server_nonce):
 | 
						|
##        return 'r={client_nonce}{server_nonce},s={salt},i={rounds}'.format(
 | 
						|
##            client_nonce=client_nonce,
 | 
						|
##            server_nonce=server_nonce,
 | 
						|
##            rounds=self.rounds,
 | 
						|
##            salt=self.encoded_salt,
 | 
						|
##            )
 | 
						|
##
 | 
						|
##    def format_auth_msg(self, username, client_nonce, server_nonce,
 | 
						|
##                        header='c=biws'):
 | 
						|
##        return (
 | 
						|
##            'n={username},r={client_nonce}'
 | 
						|
##                ','
 | 
						|
##            'r={client_nonce}{server_nonce},s={salt},i={rounds}'
 | 
						|
##                ','
 | 
						|
##            '{header},r={client_nonce}{server_nonce}'
 | 
						|
##            ).format(
 | 
						|
##                username=username,
 | 
						|
##                client_nonce=client_nonce,
 | 
						|
##                server_nonce=server_nonce,
 | 
						|
##                salt=self.encoded_salt,
 | 
						|
##                rounds=self.rounds,
 | 
						|
##                header=header,
 | 
						|
##                )
 | 
						|
##
 | 
						|
##    #=========================================================
 | 
						|
##    # helpers to calculate & cache constant data
 | 
						|
##    #=========================================================
 | 
						|
##    def _calc_get_hmac(self):
 | 
						|
##        return get_prf("hmac-" + iana_to_hashlib(self.alg))[0]
 | 
						|
##
 | 
						|
##    def _calc_client_key(self):
 | 
						|
##        return self.get_hmac(self.digest, b("Client Key"))
 | 
						|
##
 | 
						|
##    def _calc_stored_key(self):
 | 
						|
##        return self.get_hash(self.client_key)
 | 
						|
##
 | 
						|
##    def _calc_server_key(self):
 | 
						|
##        return self.get_hmac(self.digest, b("Server Key"))
 | 
						|
##
 | 
						|
##    def _calc_encoded_salt(self):
 | 
						|
##        return self.salt.encode("base64").rstrip()
 | 
						|
##
 | 
						|
##    #=========================================================
 | 
						|
##    # hacks for calculated attributes
 | 
						|
##    #=========================================================
 | 
						|
##
 | 
						|
##    def __getattr__(self, attr):
 | 
						|
##        if not attr.startswith("_"):
 | 
						|
##            f = getattr(self, "_calc_" + attr, None)
 | 
						|
##            if f:
 | 
						|
##                value = f()
 | 
						|
##                setattr(self, attr, value)
 | 
						|
##                return value
 | 
						|
##        raise AttributeError("attribute not found")
 | 
						|
##
 | 
						|
##    def __dir__(self):
 | 
						|
##        cdir = dir(self.__class__)
 | 
						|
##        attrs = set(cdir)
 | 
						|
##        attrs.update(self.__dict__)
 | 
						|
##        attrs.update(attr[6:] for attr in cdir
 | 
						|
##                     if attr.startswith("_calc_"))
 | 
						|
##        return sorted(attrs)
 | 
						|
##    #=========================================================
 | 
						|
##    # eoc
 | 
						|
##    #=========================================================
 | 
						|
 | 
						|
#=============================================================================
 | 
						|
# eof
 | 
						|
#=============================================================================
 |