1
"""passlib.handlers.fshp
4
#=============================================================================
6
#=============================================================================
8
from base64 import b64encode, b64decode
10
import logging; log = logging.getLogger(__name__)
11
from warnings import warn
14
from passlib.utils import to_unicode
15
import passlib.utils.handlers as uh
16
from passlib.utils.compat import b, bytes, bascii_to_str, iteritems, u,\
18
from passlib.utils.pbkdf2 import pbkdf1
23
#=============================================================================
25
#=============================================================================
26
class fshp(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
27
"""This class implements the FSHP password hash, and follows the :ref:`password-hash-api`.
29
It supports a variable-length salt, and a variable number of rounds.
31
The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
34
Optional raw salt string.
35
If not specified, one will be autogenerated (this is recommended).
38
Optional number of bytes to use when autogenerating new salts.
39
Defaults to 16 bytes, but can be any non-negative value.
42
Optional number of rounds to use.
43
Defaults to 50000, must be between 1 and 4294967295, inclusive.
46
Optionally specifies variant of FSHP to use.
48
* ``0`` - uses SHA-1 digest (deprecated).
49
* ``1`` - uses SHA-2/256 digest (default).
50
* ``2`` - uses SHA-2/384 digest.
51
* ``3`` - uses SHA-2/512 digest.
55
By default, providing an invalid value for one of the other
56
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
57
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
58
will be issued instead. Correctable errors include ``rounds``
59
that are too small or too large, and ``salt`` strings that are too long.
64
#===================================================================
66
#===================================================================
69
setting_kwds = ("salt", "salt_size", "rounds", "variant")
70
checksum_chars = uh.PADDED_BASE64_CHARS
72
# checksum_size is property() that depends on variant
75
default_salt_size = 16 # current passlib default, FSHP uses 8
80
# FIXME: should probably use different default rounds
81
# based on the variant. setting for default variant (sha256) for now.
82
default_rounds = 50000 # current passlib default, FSHP uses 4096
83
min_rounds = 1 # set by FSHP
84
max_rounds = 4294967295 # 32-bit integer limit - not set by FSHP
85
rounds_cost = "linear"
90
# variant: (hash name, digest size)
96
_variant_aliases = dict(
97
[(unicode(k),k) for k in _variant_info] +
98
[(v[0],k) for k,v in iteritems(_variant_info)]
101
#===================================================================
103
#===================================================================
106
#===================================================================
108
#===================================================================
109
def __init__(self, variant=None, **kwds):
110
# NOTE: variant must be set first, since it controls checksum size, etc.
111
self.use_defaults = kwds.get("use_defaults") # load this early
112
self.variant = self._norm_variant(variant)
113
super(fshp, self).__init__(**kwds)
115
def _norm_variant(self, variant):
117
if not self.use_defaults:
118
raise TypeError("no variant specified")
119
variant = self.default_variant
120
if isinstance(variant, bytes):
121
variant = variant.decode("ascii")
122
if isinstance(variant, unicode):
124
variant = self._variant_aliases[variant]
126
raise ValueError("invalid fshp variant")
127
if not isinstance(variant, int):
128
raise TypeError("fshp variant must be int or known alias")
129
if variant not in self._variant_info:
130
raise ValueError("invalid fshp variant")
134
def checksum_alg(self):
135
return self._variant_info[self.variant][0]
138
def checksum_size(self):
139
return self._variant_info[self.variant][1]
141
#===================================================================
143
#===================================================================
145
_hash_regex = re.compile(u(r"""
151
([a-zA-Z0-9+/]+={0,3}) # digest
155
def from_string(cls, hash):
156
hash = to_unicode(hash, "ascii", "hash")
157
m = cls._hash_regex.match(hash)
159
raise uh.exc.InvalidHashError(cls)
160
variant, salt_size, rounds, data = m.group(1,2,3,4)
161
variant = int(variant)
162
salt_size = int(salt_size)
165
data = b64decode(data.encode("ascii"))
167
raise uh.exc.MalformedHashError(cls)
168
salt = data[:salt_size]
169
chk = data[salt_size:]
170
return cls(salt=salt, checksum=chk, rounds=rounds, variant=variant)
173
def _stub_checksum(self):
174
return b('\x00') * self.checksum_size
177
chk = self.checksum or self._stub_checksum
179
data = bascii_to_str(b64encode(salt+chk))
180
return "{FSHP%d|%d|%d}%s" % (self.variant, len(salt), self.rounds, data)
182
#===================================================================
184
#===================================================================
186
def _calc_checksum(self, secret):
187
if isinstance(secret, unicode):
188
secret = secret.encode("utf-8")
189
# NOTE: for some reason, FSHP uses pbkdf1 with password & salt reversed.
190
# this has only a minimal impact on security,
191
# but it is worth noting this deviation.
196
keylen=self.checksum_size,
197
hash=self.checksum_alg,
200
#===================================================================
202
#===================================================================
204
#=============================================================================
206
#=============================================================================