summaryrefslogtreecommitdiff
path: root/passlib/tests/handler_utils.py
blob: 5756d713125366a85b989c10c8d562db3e7e6cd7 (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
354
355
"""tests for passlib.pwhash -- (c) Assurance Technologies 2003-2009"""
#=========================================================
#imports
#=========================================================
#core
import re
#site
from nose.plugins.skip import SkipTest
#pkg
from passlib.tests.utils import TestCase, enable_option
from passlib.utils.handlers import ExtHandler, BackendMixin
#module
__all__ = [
    "_HandlerTestCase",
    "create_backend_test",
]
#=========================================================
#other unittest helpers
#=========================================================

class _HandlerTestCase(TestCase):
    """base class for testing CryptHandler implementations.

    .. todo::
        write directions on how to use this class.
        for now, see examples in places such as test_unix_crypt
    """

    #=========================================================
    #attrs to be filled in by subclass for testing specific handler
    #=========================================================

    #specify handler object here
    handler = None

    #this option is available for hashes which can't handle unicode
    supports_unicode = True

    #maximum number of chars which hash will include in checksum
    #override this only if hash doesn't use all chars (the default)
    secret_chars = -1

    #list of (secret,hash) pairs which handler should verify as matching
    known_correct = []

    #list of (secret,hash) pairs which handler should verify as NOT matching
    known_incorrect = []

    # list of handler's hashes with crucial invalidating typos, that handler shouldn't identify as belonging to it
    known_invalid = []

    # list of handler's hashes that it *will* identify as it's own, but genhash will raise error due to invalid internal requirements
    known_identified_invalid = []

    #list of (name, hash) pairs for other algorithm's hashes, that handler shouldn't identify as belonging to it
    #this list should generally be sufficient (if handler name in list, that entry will be skipped)
    known_other = [
        ('des_crypt', '6f8c114b58f2c'),
        ('md5_crypt', '$1$dOHYPKoP$tnxS1T8Q6VVn3kpV8cN6o.'),
        ('sha512_crypt', "$6$rounds=123456$asaltof16chars..$BtCwjqMJGx5hrJhZywWvt0RLE8uZ4oPwc"
            "elCjmw2kSYu.Ec6ycULevoBK25fs2xXgMNrCzIMVcgEJAstJeonj1"),
    ]

    #list of various secrets all algs are tested with to make sure they work
    standard_secrets = [
        '',
        ' ',
        'my socrates note',
        'Compl3X AlphaNu3meric',
        '4lpHa N|_|M3r1K W/ Cur51|\\|g: #$%(*)(*%#',
        'Really Long Password (tm), which is all the rage nowadays. Maybe some Shakespeare?',
        ]

    unicode_secrets = [
        u'test with unic\u00D6de',
    ]

    #optional prefix to prepend to name of test method as it's called,
    #useful when multiple handler test classes being run.
    #default behavior should be sufficient
    def case_prefix(self):
        name = self.handler.name if self.handler else self.__class__.__name__
        backend = getattr(self.handler, "get_backend", None) #set by some of the builtin handlers
        if backend:
            name += " (%s backend)" % (backend(),)
        return name

    #=========================================================
    #alg interface helpers - allows subclass to overide how
    # default tests invoke the handler (eg for context_kwds)
    #=========================================================
    def do_concat(self, secret, prefix):
        "concatenate prefix onto secret"
        #NOTE: this is subclassable mainly for some algorithms
        #which accept non-strings in secret
        return prefix + secret

    def do_encrypt(self, secret, **kwds):
        "call handler's encrypt method with specified options"
        return self.handler.encrypt(secret, **kwds)

    def do_verify(self, secret, hash):
        "call handler's verify method"
        return self.handler.verify(secret, hash)

    def do_identify(self, hash):
        "call handler's identify method"
        return self.handler.identify(hash)

    #=========================================================
    #attributes
    #=========================================================
    def test_00_attributes(self):
        "test handler attributes are all defined"
        handler = self.handler
        def ga(name):
            return getattr(handler, name, None)

        name = ga("name")
        self.assert_(name, "name not defined:")
        self.assert_(name.lower() == name, "name not lower-case:")
        self.assert_(re.match("^[a-z0-9_]+$", name), "name must be alphanum + underscore: %r" % (name,))

    def test_01_base_handler(self):
        "run ExtHandler validation tests"
        h = self.handler
        if not isinstance(h, type) or not issubclass(h, ExtHandler):
            raise SkipTest
        h.validate_class() #should raise AssertionError if something's wrong.

    #=========================================================
    #identify
    #=========================================================
    def test_10_identify_other(self):
        "test identify() against other schemes' hashes"
        for name, hash in self.known_other:
            self.assertEqual(self.do_identify(hash), name == self.handler.name)

    def test_11_identify_positive(self):
        "test identify() against scheme's own hashes"
        for secret, hash in self.known_correct:
            self.assertEqual(self.do_identify(hash), True)

        for secret, hash in self.known_incorrect:
            self.assertEqual(self.do_identify(hash), True)

        for hash in self.known_identified_invalid:
            self.assertEqual(self.do_identify(hash), True)

    def test_12_identify_invalid(self):
        "test identify() against malformed instances of scheme's own hashes"
        if not self.known_invalid:
            raise SkipTest
        for hash in self.known_invalid:
            self.assertEqual(self.do_identify(hash), False, "hash=%r:" % (hash,))

    def test_13_identify_none(self):
        "test identify() against None / empty string"
        self.assertEqual(self.do_identify(None), False)
        self.assertEqual(self.do_identify(''), False)

    #=========================================================
    #verify
    #=========================================================
    def test_20_verify_positive(self):
        "test verify() against known-correct secret/hash pairs"
        self.assert_(self.known_correct, "test must define known_correct hashes")
        for secret, hash in self.known_correct:
            self.assertEqual(self.do_verify(secret, hash), True, "known correct hash (secret=%r, hash=%r):" % (secret,hash))

    def test_21_verify_negative(self):
        "test verify() against known-incorrect secret/hash pairs"
        if not self.known_incorrect:
            raise SkipTest
        for secret, hash in self.known_incorrect:
            self.assertEqual(self.do_verify(secret, hash), False)

    #XXX: is this needed if known_incorrect is defined?
    def test_22_verify_derived_negative(self):
        "test verify() against derived incorrect secret/hash pairs"
        for secret, hash in self.known_correct:
            self.assertEqual(self.do_verify(self.do_concat(secret,'x'), hash), False)

    def test_23_verify_other(self):
        "test verify() throws error against other algorithm's hashes"
        for name, hash in self.known_other:
            if name == self.handler.name:
                continue
            self.assertRaises(ValueError, self.do_verify, 'stub', hash, __msg__="verify other %r %r:" % (name, hash))

    def test_24_verify_invalid(self):
        "test verify() throws error against known-invalid hashes"
        if not self.known_invalid and not self.known_identified_invalid:
            raise SkipTest
        for hash in self.known_invalid + self.known_identified_invalid:
            self.assertRaises(ValueError, self.do_verify, 'stub', hash, __msg__="verify invalid %r:" % (hash,))

    def test_25_verify_none(self):
        "test verify() throws error against hash=None/empty string"
        #find valid hash so that doesn't mask error
        self.assertRaises(ValueError, self.do_verify, 'stub', None, __msg__="verify None:")
        self.assertRaises(ValueError, self.do_verify, 'stub', '', __msg__="verify empty:")

    #=========================================================
    #encrypt
    #=========================================================

    #---------------------------------------------------------
    #test encryption against various secrets
    #---------------------------------------------------------
    def test_30_encrypt_standard(self):
        "test encrypt() against standard secrets"
        for secret in self.standard_secrets:
            self.check_encrypt(secret)

    def test_31_encrypt_unicode(self):
        "test encrypt() against unicode secrets"
        if not self.supports_unicode:
            raise SkipTest
        for secret in self.unicode_secrets:
            self.check_encrypt(secret)

    #this is probably excessive
    ##def test_32_encrypt_positive(self):
    ##    "test encrypt() against known-correct secret/hash pairs"
    ##    for secret, hash in self.known_correct:
    ##        self.check_encrypt(secret)

    def check_encrypt(self, secret):
        "check encrypt() behavior for a given secret"
        #hash the secret
        hash = self.do_encrypt(secret)

        #test identification
        self.assertEqual(self.do_identify(hash), True, "identify hash %r from secret %r:" % (hash, secret))

        #test positive verification
        self.assertEqual(self.do_verify(secret, hash), True, "verify hash %r from secret %r:" % (hash, secret))

        #test negative verification
        for other in ['', 'test', self.do_concat(secret,'x')]:
            if other != secret:
                self.assertEqual(self.do_verify(other, hash), False,
                    "hash collision: %r and %r => %r" % (secret, other, hash))

    #---------------------------------------------------------
    #test salt handling
    #---------------------------------------------------------
    def test_33_encrypt_gensalt(self):
        "test encrypt() generates new salt each time"
        if 'salt' not in self.handler.setting_kwds:
            raise SkipTest
        for secret, hash in self.known_correct:
            hash2 = self.do_encrypt(secret)
            self.assertNotEqual(hash, hash2)

    #TODO: test too-short user-provided salts
    #TODO: test too-long user-provided salts
    #TODO: test invalid char in user-provided salts

    #---------------------------------------------------------
    #test secret handling
    #---------------------------------------------------------
    def test_37_secret_chars(self):
        "test secret_chars limit"
        sc = self.secret_chars

        base = "too many secrets" #16 chars
        alt = 'x' #char that's not in base string

        if sc > 0:
            #hash only counts the first <sc> characters
            #eg: bcrypt, des-crypt

            #create & hash something of exactly sc+1 chars
            secret = (base * (1+sc//16))[:sc+1]
            assert len(secret) == sc+1
            hash = self.do_encrypt(secret)

            #check sc value isn't too large
            #by verifying that sc-1'th char affects hash
            self.assert_(not self.do_verify(secret[:-2] + alt + secret[-1], hash), "secret_chars value is too large")

            #check sc value isn't too small
            #by verifying adding sc'th char doesn't affect hash
            self.assert_(self.do_verify(secret[:-1] + alt, hash))

        else:
            #hash counts all characters
            #eg: md5-crypt
            self.assertEquals(sc, -1)

            #NOTE: this doesn't do an exhaustive search to verify algorithm
            #doesn't have some cutoff point, it just tries
            #1024-character string, and alters the last char.
            #as long as algorithm doesn't clip secret at point <1024,
            #the new secret shouldn't verify.
            secret = base * 64
            hash = self.do_encrypt(secret)
            self.assert_(not self.do_verify(secret[:-1] + alt, hash))

    def test_38_encrypt_none(self):
        "test encrypt() refused secret=None"
        self.assertRaises(TypeError, self.do_encrypt, None)

    #=========================================================
    #
    #=========================================================

    #TODO: check genhash works
    #TODO: check genconfig works

    #TODO: check parse method works
    #TODO: check render method works
    #TODO: check default/min/max_rounds valid if present

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

#=========================================================
#backend test helpers
#=========================================================
def enable_backend_case(handler, name):
    "helper to check if a separate test is needed for the specified backend"
    assert issubclass(handler, BackendMixin), "handler must derived from BackendMixin"
    assert name in handler.backends, "unknown backend: %r" % (name,)
    return enable_option("all-backends") and handler.get_backend() != name and handler.has_backend(name)

def create_backend_case(base_test, name):
    "create a test case (subclassing); if test doesn't need to be enabled, returns None"
    handler = base_test.handler

    if not enable_backend_case(handler, name):
        return None

    assert getattr(base_test, "setUp", None) is None #just haven't implemented this
    assert getattr(base_test, "cleanUp", None) is None #ditto

    class dummy(base_test):
        case_prefix = "%s (%s backend)" % (handler.name, name)

        def setUp(self):
            self.orig_backend = self.handler.get_backend()
            self.handler.set_backend(name)

        def cleanUp(self):
            self.handler.set_backend(self.orig_backend)

    dummy.__name__ = name.title() + base_test.__name__
    return dummy

#=========================================================
#EOF
#=========================================================