summaryrefslogtreecommitdiff
path: root/passlib/handlers/misc.py
blob: 44abc3438a3fee5d2865498c54aa72e6fbdebda7 (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
"""passlib.handlers.misc - misc generic handlers
"""
#=============================================================================
# imports
#=============================================================================
# core
import sys
import logging; log = logging.getLogger(__name__)
from warnings import warn
# site
# pkg
from passlib.utils import to_native_str, str_consteq
from passlib.utils.compat import unicode, u, unicode_or_bytes_types
import passlib.utils.handlers as uh
# local
__all__ = [
    "unix_disabled",
    "unix_fallback",
    "plaintext",
]

#=============================================================================
# handler
#=============================================================================
class unix_fallback(uh.ifc.DisabledHash, uh.StaticHandler):
    """This class provides the fallback behavior for unix shadow files, and follows the :ref:`password-hash-api`.

    This class does not implement a hash, but instead provides fallback
    behavior as found in /etc/shadow on most unix variants.
    If used, should be the last scheme in the context.

    * this class will positively identify all hash strings.
    * for security, passwords will always hash to ``!``.
    * it rejects all passwords if the hash is NOT an empty string (``!`` or ``*`` are frequently used).
    * by default it rejects all passwords if the hash is an empty string,
      but if ``enable_wildcard=True`` is passed to verify(),
      all passwords will be allowed through if the hash is an empty string.

    .. deprecated:: 1.6
        This has been deprecated due to its "wildcard" feature,
        and will be removed in Passlib 1.8. Use :class:`unix_disabled` instead.
    """
    name = "unix_fallback"
    context_kwds = ("enable_wildcard",)

    @classmethod
    def identify(cls, hash):
        if isinstance(hash, unicode_or_bytes_types):
            return True
        else:
            raise uh.exc.ExpectedStringError(hash, "hash")

    def __init__(self, enable_wildcard=False, **kwds):
        warn("'unix_fallback' is deprecated, "
             "and will be removed in Passlib 1.8; "
             "please use 'unix_disabled' instead.",
             DeprecationWarning)
        super(unix_fallback, self).__init__(**kwds)
        self.enable_wildcard = enable_wildcard

    def _calc_checksum(self, secret):
        if self.checksum:
            # NOTE: hash will generally be "!", but we want to preserve
            # it in case it's something else, like "*".
            return self.checksum
        else:
            return u("!")

    @classmethod
    def verify(cls, secret, hash, enable_wildcard=False):
        uh.validate_secret(secret)
        if not isinstance(hash, unicode_or_bytes_types):
            raise uh.exc.ExpectedStringError(hash, "hash")
        elif hash:
            return False
        else:
            return enable_wildcard

_MARKER_CHARS = u("*!")
_MARKER_BYTES = b"*!"

class unix_disabled(uh.ifc.DisabledHash, uh.MinimalHandler):
    """This class provides disabled password behavior for unix shadow files,
    and follows the :ref:`password-hash-api`.

    This class does not implement a hash, but instead matches the "disabled account"
    strings found in ``/etc/shadow`` on most Unix variants. "encrypting" a password
    will simply return the disabled account marker. It will reject all passwords,
    no matter the hash string. The :meth:`~passlib.ifc.PasswordHash.hash`
    method supports one optional keyword:

    :type marker: str
    :param marker:
        Optional marker string which overrides the platform default
        used to indicate a disabled account.

        If not specified, this will default to ``"*"`` on BSD systems,
        and use the Linux default ``"!"`` for all other platforms.
        (:attr:`!unix_disabled.default_marker` will contain the default value)

    .. versionadded:: 1.6
        This class was added as a replacement for the now-deprecated
        :class:`unix_fallback` class, which had some undesirable features.
    """
    name = "unix_disabled"
    setting_kwds = ("marker",)
    context_kwds = ()

    _disable_prefixes = tuple(str(_MARKER_CHARS))

    # TODO: rename attr to 'marker'...
    if 'bsd' in sys.platform: # pragma: no cover -- runtime detection
        default_marker = u("*")
    else:
        # use the linux default for other systems
        # (glibc also supports adding old hash after the marker
        # so it can be restored later).
        default_marker = u("!")

    @classmethod
    def using(cls, marker=None, **kwds):
        subcls = super(unix_disabled, cls).using(**kwds)
        if marker is not None:
            if not cls.identify(marker):
                raise ValueError("invalid marker: %r" % marker)
            subcls.default_marker = marker
        return subcls

    @classmethod
    def identify(cls, hash):
        # NOTE: technically, anything in the /etc/shadow password field
        #       which isn't valid crypt() output counts as "disabled".
        #       but that's rather ambiguous, and it's hard to predict what
        #       valid output is for unknown crypt() implementations.
        #       so to be on the safe side, we only match things *known*
        #       to be disabled field indicators, and will add others
        #       as they are found. things beginning w/ "$" should *never* match.
        #
        # things currently matched:
        #       * linux uses "!"
        #       * bsd uses "*"
        #       * linux may use "!" + hash to disable but preserve original hash
        #       * linux counts empty string as "any password";
        #         this code recognizes it, but treats it the same as "!"
        if isinstance(hash, unicode):
            start = _MARKER_CHARS
        elif isinstance(hash, bytes):
            start = _MARKER_BYTES
        else:
            raise uh.exc.ExpectedStringError(hash, "hash")
        return not hash or hash[0] in start

    @classmethod
    def verify(cls, secret, hash):
        uh.validate_secret(secret)
        if not cls.identify(hash): # handles typecheck
            raise uh.exc.InvalidHashError(cls)
        return False

    @classmethod
    def hash(cls, secret, **kwds):
        if kwds:
            uh.warn_hash_settings_deprecation(cls, kwds)
            return cls.using(**kwds).hash(secret)
        uh.validate_secret(secret)
        marker = cls.default_marker
        assert marker and cls.identify(marker)
        return to_native_str(marker, param="marker")

    @uh.deprecated_method(deprecated="1.7", removed="2.0")
    @classmethod
    def genhash(cls, secret, config, marker=None):
        if not cls.identify(config):
            raise uh.exc.InvalidHashError(cls)
        elif config:
            # preserve the existing str,since it might contain a disabled password hash ("!" + hash)
            uh.validate_secret(secret)
            return to_native_str(config, param="config")
        else:
            if marker is not None:
                cls = cls.using(marker=marker)
            return cls.hash(secret)

    @classmethod
    def disable(cls, hash=None):
        out = cls.hash("")
        if hash is not None:
            hash = to_native_str(hash, param="hash")
            if cls.identify(hash):
                # extract original hash, so that we normalize marker
                hash = cls.enable(hash)
            if hash:
                out += hash
        return out

    @classmethod
    def enable(cls, hash):
        hash = to_native_str(hash, param="hash")
        for prefix in cls._disable_prefixes:
            if hash.startswith(prefix):
                orig = hash[len(prefix):]
                if orig:
                    return orig
                else:
                    raise ValueError("cannot restore original hash")
        raise uh.exc.InvalidHashError(cls)

class plaintext(uh.MinimalHandler):
    """This class stores passwords in plaintext, and follows the :ref:`password-hash-api`.

    The :meth:`~passlib.ifc.PasswordHash.hash`, :meth:`~passlib.ifc.PasswordHash.genhash`, and :meth:`~passlib.ifc.PasswordHash.verify` methods all require the
    following additional contextual keyword:

    :type encoding: str
    :param encoding:
        This controls the character encoding to use (defaults to ``utf-8``).

        This encoding will be used to encode :class:`!unicode` passwords
        under Python 2, and decode :class:`!bytes` hashes under Python 3.

    .. versionchanged:: 1.6
        The ``encoding`` keyword was added.
    """
    # NOTE: this is subclassed by ldap_plaintext

    name = "plaintext"
    setting_kwds = ()
    context_kwds = ("encoding",)
    default_encoding = "utf-8"

    @classmethod
    def identify(cls, hash):
        if isinstance(hash, unicode_or_bytes_types):
            return True
        else:
            raise uh.exc.ExpectedStringError(hash, "hash")

    @classmethod
    def hash(cls, secret, encoding=None):
        uh.validate_secret(secret)
        if not encoding:
            encoding = cls.default_encoding
        return to_native_str(secret, encoding, "secret")

    @classmethod
    def verify(cls, secret, hash, encoding=None):
        if not encoding:
            encoding = cls.default_encoding
        hash = to_native_str(hash, encoding, "hash")
        if not cls.identify(hash):
            raise uh.exc.InvalidHashError(cls)
        return str_consteq(cls.hash(secret, encoding), hash)

    @uh.deprecated_method(deprecated="1.7", removed="2.0")
    @classmethod
    def genconfig(cls):
        return cls.hash("")

    @uh.deprecated_method(deprecated="1.7", removed="2.0")
    @classmethod
    def genhash(cls, secret, config, encoding=None):
        # NOTE: 'config' is ignored, as this hash has no salting / etc
        if not cls.identify(config):
            raise uh.exc.InvalidHashError(cls)
        return cls.hash(secret, encoding=encoding)

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