summaryrefslogtreecommitdiff
path: root/passlib/ifc.py
blob: 559d256e3d684fa3846320e6a52ac634a08f0d0f (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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
"""passlib.ifc - abstract interfaces used by Passlib"""
#=============================================================================
# imports
#=============================================================================
# core
import logging; log = logging.getLogger(__name__)
import sys
# site
# pkg
from passlib.utils.decor import deprecated_method
# local
__all__ = [
    "PasswordHash",
]

#=============================================================================
# 2/3 compatibility helpers
#=============================================================================
def recreate_with_metaclass(meta):
    """class decorator that re-creates class using metaclass"""
    def builder(cls):
        if meta is type(cls):
            return cls
        return meta(cls.__name__, cls.__bases__, cls.__dict__.copy())
    return builder

#=============================================================================
# PasswordHash interface
#=============================================================================
from abc import ABCMeta, abstractmethod, abstractproperty

# TODO: make this actually use abstractproperty(),
#       now that we dropped py25, 'abc' is always available.

# XXX: rename to PasswordHasher?

@recreate_with_metaclass(ABCMeta)
class PasswordHash(object):
    """This class describes an abstract interface which all password hashes
    in Passlib adhere to. Under Python 2.6 and up, this is an actual
    Abstract Base Class built using the :mod:`!abc` module.

    See the Passlib docs for full documentation.
    """
    #===================================================================
    # class attributes
    #===================================================================

    #---------------------------------------------------------------
    # general information
    #---------------------------------------------------------------
    ##name
    ##setting_kwds
    ##context_kwds

    #: flag which indicates this hasher matches a "disabled" hash
    #: (e.g. unix_disabled, or django_disabled); and doesn't actually
    #: depend on the provided password.
    is_disabled = False

    #: Should be None, or a positive integer indicating hash
    #: doesn't support secrets larger than this value.
    #: Whether hash throws error or silently truncates secret
    #: depends on .truncate_error and .truncate_verify_reject flags below.
    #: NOTE: calls may treat as boolean, since value will never be 0.
    #: .. versionadded:: 1.7
    #: .. TODO: passlib 1.8: deprecate/rename this attr to "max_secret_size"?
    truncate_size = None

    # NOTE: these next two default to the optimistic "ideal",
    #       most hashes in passlib have to default to False
    #       for backward compat and/or expected behavior with existing hashes.

    #: If True, .hash() should throw a :exc:`~passlib.exc.PasswordSizeError` for
    #: any secrets larger than .truncate_size.  Many hashers default to False
    #: for historical / compatibility purposes, indicating they will silently
    #: truncate instead.  All such hashers SHOULD support changing
    #: the policy via ``.using(truncate_error=True)``.
    #: .. versionadded:: 1.7
    #: .. TODO: passlib 1.8: deprecate/rename this attr to "truncate_hash_error"?
    truncate_error = True

    #: If True, .verify() should reject secrets larger than max_password_size.
    #: Many hashers default to False for historical / compatibility purposes,
    #: indicating they will match on the truncated portion instead.
    #: .. versionadded:: 1.7.1
    truncate_verify_reject = True

    #---------------------------------------------------------------
    # salt information -- if 'salt' in setting_kwds
    #---------------------------------------------------------------
    ##min_salt_size
    ##max_salt_size
    ##default_salt_size
    ##salt_chars
    ##default_salt_chars

    #---------------------------------------------------------------
    # rounds information -- if 'rounds' in setting_kwds
    #---------------------------------------------------------------
    ##min_rounds
    ##max_rounds
    ##default_rounds
    ##rounds_cost

    #---------------------------------------------------------------
    # encoding info -- if 'encoding' in context_kwds
    #---------------------------------------------------------------
    ##default_encoding

    #===================================================================
    # primary methods
    #===================================================================
    @classmethod
    @abstractmethod
    def hash(cls, secret,  # *
             **setting_and_context_kwds):  # pragma: no cover -- abstract method
        r"""
        Hash secret, returning result.
        Should handle generating salt, etc, and should return string
        containing identifier, salt & other configuration, as well as digest.

        :param \\*\\*settings_kwds:

            Pass in settings to customize configuration of resulting hash.

            .. deprecated:: 1.7

                Starting with Passlib 1.7, callers should no longer pass settings keywords
                (e.g. ``rounds`` or ``salt`` directly to :meth:`!hash`); should use
                ``.using(**settings).hash(secret)`` construction instead.

                Support will be removed in Passlib 2.0.

        :param \\*\\*context_kwds:

            Specific algorithms may require context-specific information (such as the user login).
        """
        # FIXME:  need stub for classes that define .encrypt() instead ...
        #         this should call .encrypt(), and check for recursion back to here.
        raise NotImplementedError("must be implemented by subclass")

    @deprecated_method(deprecated="1.7", removed="2.0", replacement=".hash()")
    @classmethod
    def encrypt(cls, *args, **kwds):
        """
        Legacy alias for :meth:`hash`.

        .. deprecated:: 1.7
            This method was renamed to :meth:`!hash` in version 1.7.
            This alias will be removed in version 2.0, and should only
            be used for compatibility with Passlib 1.3 - 1.6.
        """
        return cls.hash(*args, **kwds)

    # XXX: could provide default implementation which hands value to
    #      hash(), and then does constant-time comparision on the result
    #      (after making both are same string type)
    @classmethod
    @abstractmethod
    def verify(cls, secret, hash, **context_kwds): # pragma: no cover -- abstract method
        """verify secret against hash, returns True/False"""
        raise NotImplementedError("must be implemented by subclass")

    #===================================================================
    # configuration
    #===================================================================
    @classmethod
    @abstractmethod
    def using(cls, relaxed=False, **kwds):
        """
        Return another hasher object (typically a subclass of the current one),
        which integrates the configuration options specified by ``kwds``.
        This should *always* return a new object, even if no configuration options are changed.

        .. todo::

            document which options are accepted.

        :returns:
            typically returns a subclass for most hasher implementations.

        .. todo::

            add this method to main documentation.
        """
        raise NotImplementedError("must be implemented by subclass")

    #===================================================================
    # migration
    #===================================================================
    @classmethod
    def needs_update(cls, hash, secret=None):
        """
        check if hash's configuration is outside desired bounds,
        or contains some other internal option which requires
        updating the password hash.

        :param hash:
            hash string to examine

        :param secret:
            optional secret known to have verified against the provided hash.
            (this is used by some hashes to detect legacy algorithm mistakes).

        :return:
            whether secret needs re-hashing.

        .. versionadded:: 1.7
        """
        # by default, always report that we don't need update
        return False

    #===================================================================
    # additional methods
    #===================================================================
    @classmethod
    @abstractmethod
    def identify(cls, hash): # pragma: no cover -- abstract method
        """check if hash belongs to this scheme, returns True/False"""
        raise NotImplementedError("must be implemented by subclass")

    @deprecated_method(deprecated="1.7", removed="2.0")
    @classmethod
    def genconfig(cls, **setting_kwds): # pragma: no cover -- abstract method
        """
        compile settings into a configuration string for genhash()

        .. deprecated:: 1.7

            As of 1.7, this method is deprecated, and slated for complete removal in Passlib 2.0.

            For all known real-world uses, hashing a constant string
            should provide equivalent functionality.

            This deprecation may be reversed if a use-case presents itself in the mean time.
        """
        # NOTE: this fallback runs full hash alg, w/ whatever cost param is passed along.
        #       implementations (esp ones w/ variable cost) will want to subclass this
        #       with a constant-time implementation that just renders a config string.
        if cls.context_kwds:
            raise NotImplementedError("must be implemented by subclass")
        return cls.using(**setting_kwds).hash("")

    @deprecated_method(deprecated="1.7", removed="2.0")
    @classmethod
    def genhash(cls, secret, config, **context):
        """
        generated hash for secret, using settings from config/hash string

        .. deprecated:: 1.7

            As of 1.7, this method is deprecated, and slated for complete removal in Passlib 2.0.

            This deprecation may be reversed if a use-case presents itself in the mean time.
        """
        # XXX: if hashes reliably offered a .parse() method, could make a fallback for this.
        raise NotImplementedError("must be implemented by subclass")

    #===================================================================
    # undocumented methods / attributes
    #===================================================================
    # the following entry points are used internally by passlib,
    # and aren't documented as part of the exposed interface.
    # they are subject to change between releases,
    # but are documented here so there's a list of them *somewhere*.

    #---------------------------------------------------------------
    # extra metdata
    #---------------------------------------------------------------

    #: this attribute shouldn't be used by hashers themselves,
    #: it's reserved for the CryptContext to track which hashers are deprecated.
    #: Note the context will only set this on objects it owns (and generated by .using()),
    #: and WONT set it on global objects.
    #: [added in 1.7]
    #: TODO: document this, or at least the use of testing for
    #:       'CryptContext().handler().deprecated'
    deprecated = False

    #: optionally present if hasher corresponds to format built into Django.
    #: this attribute (if not None) should be the Django 'algorithm' name.
    #: also indicates to passlib.ext.django that (when installed in django),
    #: django's native hasher should be used in preference to this one.
    ## django_name

    #---------------------------------------------------------------
    # checksum information - defined for many hashes
    #---------------------------------------------------------------
    ## checksum_chars
    ## checksum_size

    #---------------------------------------------------------------
    # experimental methods
    #---------------------------------------------------------------

    ##@classmethod
    ##def normhash(cls, hash):
    ##    """helper to clean up non-canonic instances of hash.
    ##    currently only provided by bcrypt() to fix an historical passlib issue.
    ##    """

    # experimental helper to parse hash into components.
    ##@classmethod
    ##def parsehash(cls, hash, checksum=True, sanitize=False):
    ##    """helper to parse hash into components, returns dict"""

    # experiment helper to estimate bitsize of different hashes,
    # implement for GenericHandler, but may be currently be off for some hashes.
    # want to expand this into a way to programmatically compare
    # "strengths" of different hashes and hash algorithms.
    # still needs to have some factor for estimate relative cost per round,
    # ala in the style of the scrypt whitepaper.
    ##@classmethod
    ##def bitsize(cls, **kwds):
    ##    """returns dict mapping component -> bits contributed.
    ##    components currently include checksum, salt, rounds.
    ##    """

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

class DisabledHash(PasswordHash):
    """
    extended disabled-hash methods; only need be present if .disabled = True
    """

    is_disabled = True

    @classmethod
    def disable(cls, hash=None):
        """
        return string representing a 'disabled' hash;
        optionally including previously enabled hash
        (this is up to the individual scheme).
        """
        # default behavior: ignore original hash, return standalone marker
        return cls.hash("")

    @classmethod
    def enable(cls, hash):
        """
        given a disabled-hash string,
        extract previously-enabled hash if one is present,
        otherwise raises ValueError
        """
        # default behavior: no way to restore original hash
        raise ValueError("cannot restore original hash")

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