summaryrefslogtreecommitdiff
path: root/passlib/handlers/phpass.py
blob: bb9bfd97f06443f921ec40c301dd6da5df2f223a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
"""passlib.handlers.phpass - PHPass Portable Crypt

phppass located - http://www.openwall.com/phpass/
algorithm described - http://www.openwall.com/articles/PHP-Users-Passwords

phpass context - blowfish, ext_des_crypt, phpass
"""
#=========================================================
#imports
#=========================================================
#core
from hashlib import md5
import re
import logging; log = logging.getLogger(__name__)
from warnings import warn
#site
#libs
from passlib.utils import h64
from passlib.utils.handlers import ExtendedHandler
#pkg
#local
__all__ = [
    "phpass",
]

#=========================================================
#phpass
#=========================================================
class phpass(ExtendedHandler):
    """This class implements the PHPass Portable Hash, and follows the :ref:`password-hash-api`.

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

    The :meth:`encrypt()` and :meth:`genconfig` methods accept the following optional keywords:

    :param salt:
        Optional salt string.
        If not specified, one will be autogenerated (this is recommended).
        If specified, it must be 8 characters, drawn from the regexp range ``[./0-9A-Za-z]``.

    :param rounds:
        Optional number of rounds to use.
        Defaults to 9, must be between 7 and 30, inclusive.
        This value is logarithmic, the actual number of iterations used will be :samp:`2**{rounds}`.

    :param ident:
        phpBB3 uses ``H`` instead of ``P`` for it's identifier,
        this may be set to ``H`` in order to generate phpBB3 compatible hashes.
        it defaults to ``P``.

    """

    #=========================================================
    #class attrs
    #=========================================================
    name = "phpass"
    setting_kwds = ("salt", "rounds", "ident")

    min_salt_chars = max_salt_chars = 8

    default_rounds = 9
    min_rounds = 7
    max_rounds = 30
    rounds_cost = "log2"

    _strict_rounds_bounds = True
    _extra_init_settings = ("ident",)

    #=========================================================
    #instance attrs
    #=========================================================
    ident = None

    #=========================================================
    #init
    #=========================================================
    @classmethod
    def norm_ident(cls, ident, strict=False):
        if not ident:
            if strict:
                raise ValueError("no ident specified")
            ident = "P"
        if ident not in ("P", "H"):
            raise ValueError("invalid ident: %r" % (ident,))
        return ident

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

    @classmethod
    def identify(cls, hash):
        return bool(hash) and (hash.startswith("$P$") or hash.startswith("$H$"))

    #$P$9IQRaTwmfeRo7ud9Fh4E2PdI0S3r.L0
    # $P$
    # 9
    # IQRaTwmf
    # eRo7ud9Fh4E2PdI0S3r.L0
    _pat = re.compile(r"""
        ^
        \$
        (?P<ident>[PH])
        \$
        (?P<rounds>[A-Za-z0-9./])
        (?P<salt>[A-Za-z0-9./]{8})
        (?P<chk>[A-Za-z0-9./]{22})?
        $
        """, re.X)

    @classmethod
    def from_string(cls, hash):
        if not hash:
            raise ValueError("no hash specified")
        m = cls._pat.match(hash)
        if not m:
            raise ValueError("invalid phpass portable hash")
        ident, rounds, salt, chk = m.group("ident", "rounds", "salt", "chk")
        return cls(
            ident=ident,
            rounds=h64.decode_6bit(rounds),
            salt=salt,
            checksum=chk,
            strict=bool(chk),
        )

    def to_string(self):
        return "$%s$%s%s%s" % (self.ident, h64.encode_6bit(self.rounds), self.salt, self.checksum or '')

    #=========================================================
    #backend
    #=========================================================
    def calc_checksum(self, secret):
        #FIXME: can't find definitive policy on how phpass handles non-ascii.
        if isinstance(secret, unicode):
            secret = secret.encode("utf-8")
        real_rounds = 1<<self.rounds
        result = md5(self.salt + secret).digest()
        r = 0
        while r < real_rounds:
            result = md5(result + secret).digest()
            r += 1
        return h64.encode_bytes(result)

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

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