Exemplo n.º 1
0
def raw_sun_md5_crypt(secret, rounds, salt):
    "given secret & salt, return encoded sun-md5-crypt checksum"
    global MAGIC_HAMLET
    assert isinstance(secret, bytes)
    assert isinstance(salt, bytes)

    # validate rounds
    if rounds <= 0:
        rounds = 0
    real_rounds = 4096 + rounds
    # NOTE: spec seems to imply max 'rounds' is 2**32-1

    # generate initial digest to start off round 0.
    # NOTE: algorithm 'salt' includes full config string w/ trailing "$"
    result = md5(secret + salt).digest()
    assert len(result) == 16

    # NOTE: many things in this function have been inlined (to speed up the loop
    #       as much as possible), to the point that this code barely resembles
    #       the algorithm as described in the docs. in particular:
    #
    #       * all accesses to a given bit have been inlined using the formula
    #         rbitval(bit) = (rval((bit>>3) & 15) >> (bit & 7)) & 1
    #
    #       * the calculation of coinflip value R has been inlined
    #
    #       * the conditional division of coinflip value V has been inlined as
    #         a shift right of 0 or 1.
    #
    #       * the i, i+3, etc iterations are precalculated in lists.
    #
    #       * the round-based conditional division of x & y is now performed
    #         by choosing an appropriate precalculated list, so that it only
    #         calculates the 7 bits which will actually be used.
    #
    X_ROUNDS_0, X_ROUNDS_1, Y_ROUNDS_0, Y_ROUNDS_1 = _XY_ROUNDS

    # NOTE: % appears to be *slightly* slower than &, so we prefer & if possible

    round = 0
    while round < real_rounds:
        # convert last result byte string to list of byte-ints for easy access
        rval = [byte_elem_value(c) for c in result].__getitem__

        # build up X bit by bit
        x = 0
        xrounds = X_ROUNDS_1 if (rval((round >> 3) & 15) >> (round & 7)) & 1 else X_ROUNDS_0
        for i, ia, ib in xrounds:
            a = rval(ia)
            b = rval(ib)
            v = rval((a >> (b % 5)) & 15) >> ((b >> (a & 7)) & 1)
            x |= ((rval((v >> 3) & 15) >> (v & 7)) & 1) << i

        # build up Y bit by bit
        y = 0
        yrounds = Y_ROUNDS_1 if (rval(((round + 64) >> 3) & 15) >> (round & 7)) & 1 else Y_ROUNDS_0
        for i, ia, ib in yrounds:
            a = rval(ia)
            b = rval(ib)
            v = rval((a >> (b % 5)) & 15) >> ((b >> (a & 7)) & 1)
            y |= ((rval((v >> 3) & 15) >> (v & 7)) & 1) << i

        # extract x'th and y'th bit, xoring them together to yeild "coin flip"
        coin = ((rval(x >> 3) >> (x & 7)) ^ (rval(y >> 3) >> (y & 7))) & 1

        # construct hash for this round
        h = md5(result)
        if coin:
            h.update(MAGIC_HAMLET)
        h.update(unicode(round).encode("ascii"))
        result = h.digest()

        round += 1

    # encode output
    return h64.encode_transposed_bytes(result, _chk_offsets)
Exemplo n.º 2
0
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,
            )
Exemplo n.º 3
0
    def from_string(cls, hash):
        hash = to_unicode(hash, "ascii", "hash")

        #
        # detect if hash specifies rounds value.
        # if so, parse and validate it.
        # by end, set 'rounds' to int value, and 'tail' containing salt+chk
        #
        if hash.startswith(u("$md5$")):
            rounds = 0
            salt_idx = 5
        elif hash.startswith(u("$md5,rounds=")):
            idx = hash.find(u("$"), 12)
            if idx == -1:
                raise uh.exc.MalformedHashError(cls, "unexpected end of rounds")
            rstr = hash[12:idx]
            try:
                rounds = int(rstr)
            except ValueError:
                raise uh.exc.MalformedHashError(cls, "bad rounds")
            if rstr != unicode(rounds):
                raise uh.exc.ZeroPaddedRoundsError(cls)
            if rounds == 0:
                # NOTE: not sure if this is forbidden by spec or not;
                #      but allowing it would complicate things,
                #      and it should never occur anyways.
                raise uh.exc.MalformedHashError(cls, "explicit zero rounds")
            salt_idx = idx + 1
        else:
            raise uh.exc.InvalidHashError(cls)

        #
        # salt/checksum separation is kinda weird,
        # to deal cleanly with some backward-compatible workarounds
        # implemented by original implementation.
        #
        chk_idx = hash.rfind(u("$"), salt_idx)
        if chk_idx == -1:
            # ''-config for $-hash
            salt = hash[salt_idx:]
            chk = None
            bare_salt = True
        elif chk_idx == len(hash) - 1:
            if chk_idx > salt_idx and hash[-2] == u("$"):
                raise uh.exc.MalformedHashError(cls, "too many '$' separators")
            # $-config for $$-hash
            salt = hash[salt_idx:-1]
            chk = None
            bare_salt = False
        elif chk_idx > 0 and hash[chk_idx - 1] == u("$"):
            # $$-hash
            salt = hash[salt_idx : chk_idx - 1]
            chk = hash[chk_idx + 1 :]
            bare_salt = False
        else:
            # $-hash
            salt = hash[salt_idx:chk_idx]
            chk = hash[chk_idx + 1 :]
            bare_salt = True

        return cls(rounds=rounds, salt=salt, checksum=chk, bare_salt=bare_salt)
Exemplo n.º 4
0
def raw_sun_md5_crypt(secret, rounds, salt):
    """given secret & salt, return encoded sun-md5-crypt checksum"""
    global MAGIC_HAMLET
    assert isinstance(secret, bytes)
    assert isinstance(salt, bytes)

    # validate rounds
    if rounds <= 0:
        rounds = 0
    real_rounds = 4096 + rounds
    # NOTE: spec seems to imply max 'rounds' is 2**32-1

    # generate initial digest to start off round 0.
    # NOTE: algorithm 'salt' includes full config string w/ trailing "$"
    result = md5(secret + salt).digest()
    assert len(result) == 16

    # NOTE: many things in this function have been inlined (to speed up the loop
    #       as much as possible), to the point that this code barely resembles
    #       the algorithm as described in the docs. in particular:
    #
    #       * all accesses to a given bit have been inlined using the formula
    #         rbitval(bit) = (rval((bit>>3) & 15) >> (bit & 7)) & 1
    #
    #       * the calculation of coinflip value R has been inlined
    #
    #       * the conditional division of coinflip value V has been inlined as
    #         a shift right of 0 or 1.
    #
    #       * the i, i+3, etc iterations are precalculated in lists.
    #
    #       * the round-based conditional division of x & y is now performed
    #         by choosing an appropriate precalculated list, so that it only
    #         calculates the 7 bits which will actually be used.
    #
    X_ROUNDS_0, X_ROUNDS_1, Y_ROUNDS_0, Y_ROUNDS_1 = _XY_ROUNDS

    # NOTE: % appears to be *slightly* slower than &, so we prefer & if possible

    round = 0
    while round < real_rounds:
        # convert last result byte string to list of byte-ints for easy access
        rval = [ byte_elem_value(c) for c in result ].__getitem__

        # build up X bit by bit
        x = 0
        xrounds = X_ROUNDS_1 if (rval((round>>3) & 15)>>(round & 7)) & 1 else X_ROUNDS_0
        for i, ia, ib in xrounds:
            a = rval(ia)
            b = rval(ib)
            v = rval((a >> (b % 5)) & 15) >> ((b>>(a&7)) & 1)
            x |= ((rval((v>>3)&15)>>(v&7))&1) << i

        # build up Y bit by bit
        y = 0
        yrounds = Y_ROUNDS_1 if (rval(((round+64)>>3) & 15)>>(round & 7)) & 1 else Y_ROUNDS_0
        for i, ia, ib in yrounds:
            a = rval(ia)
            b = rval(ib)
            v = rval((a >> (b % 5)) & 15) >> ((b>>(a&7)) & 1)
            y |= ((rval((v>>3)&15)>>(v&7))&1) << i

        # extract x'th and y'th bit, xoring them together to yeild "coin flip"
        coin = ((rval(x>>3) >> (x&7)) ^ (rval(y>>3) >> (y&7))) & 1

        # construct hash for this round
        h = md5(result)
        if coin:
            h.update(MAGIC_HAMLET)
        h.update(unicode(round).encode("ascii"))
        result = h.digest()

        round += 1

    # encode output
    return h64.encode_transposed_bytes(result, _chk_offsets)
Exemplo n.º 5
0
    def from_string(cls, hash):
        hash = to_unicode(hash, "ascii", "hash")

        #
        # detect if hash specifies rounds value.
        # if so, parse and validate it.
        # by end, set 'rounds' to int value, and 'tail' containing salt+chk
        #
        if hash.startswith(u("$md5$")):
            rounds = 0
            salt_idx = 5
        elif hash.startswith(u("$md5,rounds=")):
            idx = hash.find(u("$"), 12)
            if idx == -1:
                raise uh.exc.MalformedHashError(cls, "unexpected end of rounds")
            rstr = hash[12:idx]
            try:
                rounds = int(rstr)
            except ValueError:
                raise uh.exc.MalformedHashError(cls, "bad rounds")
            if rstr != unicode(rounds):
                raise uh.exc.ZeroPaddedRoundsError(cls)
            if rounds == 0:
                # NOTE: not sure if this is forbidden by spec or not;
                #      but allowing it would complicate things,
                #      and it should never occur anyways.
                raise uh.exc.MalformedHashError(cls, "explicit zero rounds")
            salt_idx = idx+1
        else:
            raise uh.exc.InvalidHashError(cls)

        #
        # salt/checksum separation is kinda weird,
        # to deal cleanly with some backward-compatible workarounds
        # implemented by original implementation.
        #
        chk_idx = hash.rfind(u("$"), salt_idx)
        if chk_idx == -1:
            # ''-config for $-hash
            salt = hash[salt_idx:]
            chk = None
            bare_salt = True
        elif chk_idx == len(hash)-1:
            if chk_idx > salt_idx and hash[-2] == u("$"):
                raise uh.exc.MalformedHashError(cls, "too many '$' separators")
            # $-config for $$-hash
            salt = hash[salt_idx:-1]
            chk = None
            bare_salt = False
        elif chk_idx > 0 and hash[chk_idx-1] == u("$"):
            # $$-hash
            salt = hash[salt_idx:chk_idx-1]
            chk = hash[chk_idx+1:]
            bare_salt = False
        else:
            # $-hash
            salt = hash[salt_idx:chk_idx]
            chk = hash[chk_idx+1:]
            bare_salt = True

        return cls(
            rounds=rounds,
            salt=salt,
            checksum=chk,
            bare_salt=bare_salt,
        )
Exemplo n.º 6
0
 def _calc_checksum(self, secret):
     return unicode(secret[0:1])
Exemplo n.º 7
0
 def _calc_checksum(self, secret):
     return unicode(secret[0:1])