summaryrefslogtreecommitdiff
path: root/passlib/tests/test_crypto_scrypt.py
blob: a3c8eefa41c3182624b3ce34c451f3ebaaa0a9e3 (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
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
"""tests for passlib.utils.scrypt"""
#=============================================================================
# imports
#=============================================================================
# core
from binascii import hexlify
import hashlib
import logging; log = logging.getLogger(__name__)
import struct
import warnings
warnings.filterwarnings("ignore", ".*using builtin scrypt backend.*")
# site
# pkg
from passlib import exc
from passlib.utils import getrandbytes
from passlib.utils.compat import PYPY, bascii_to_str
from passlib.utils.decor import classproperty
from passlib.tests.utils import TestCase, skipUnless, TEST_MODE, hb
# subject
from passlib.crypto import scrypt as scrypt_mod
# local
__all__ = [
    "ScryptEngineTest",
    "BuiltinScryptTest",
    "FastScryptTest",
]

#=============================================================================
# support functions
#=============================================================================
def hexstr(data):
    """return bytes as hex str"""
    return bascii_to_str(hexlify(data))

def unpack_uint32_list(data, check_count=None):
    """unpack bytes as list of uint32 values"""
    count = len(data) // 4
    assert check_count is None or check_count == count
    return struct.unpack("<%dI" % count, data)

def seed_bytes(seed, count):
    """
    generate random reference bytes from specified seed.
    used to generate some predictable test vectors.
    """
    if hasattr(seed, "encode"):
        seed = seed.encode("ascii")
    buf = b''
    i = 0
    while len(buf) < count:
        buf += hashlib.sha256(seed + struct.pack("<I", i)).digest()
        i += 1
    return buf[:count]

#=============================================================================
# test builtin engine's internals
#=============================================================================
class ScryptEngineTest(TestCase):
    descriptionPrefix = "passlib.crypto.scrypt._builtin"

    def test_smix(self):
        """smix()"""
        from passlib.crypto.scrypt._builtin import ScryptEngine
        rng = self.getRandom()

        #-----------------------------------------------------------------------
        # test vector from (expired) scrypt rfc draft
        # (https://tools.ietf.org/html/draft-josefsson-scrypt-kdf-01, section 9)
        #-----------------------------------------------------------------------

        input = hb("""
            f7 ce 0b 65 3d 2d 72 a4 10 8c f5 ab e9 12 ff dd
            77 76 16 db bb 27 a7 0e 82 04 f3 ae 2d 0f 6f ad
            89 f6 8f 48 11 d1 e8 7b cc 3b d7 40 0a 9f fd 29
            09 4f 01 84 63 95 74 f3 9a e5 a1 31 52 17 bc d7
            89 49 91 44 72 13 bb 22 6c 25 b5 4d a8 63 70 fb
            cd 98 43 80 37 46 66 bb 8f fc b5 bf 40 c2 54 b0
            67 d2 7c 51 ce 4a d5 fe d8 29 c9 0b 50 5a 57 1b
            7f 4d 1c ad 6a 52 3c da 77 0e 67 bc ea af 7e 89
            """)

        output = hb("""
            79 cc c1 93 62 9d eb ca 04 7f 0b 70 60 4b f6 b6
            2c e3 dd 4a 96 26 e3 55 fa fc 61 98 e6 ea 2b 46
            d5 84 13 67 3b 99 b0 29 d6 65 c3 57 60 1f b4 26
            a0 b2 f4 bb a2 00 ee 9f 0a 43 d1 9b 57 1a 9c 71
            ef 11 42 e6 5d 5a 26 6f dd ca 83 2c e5 9f aa 7c
            ac 0b 9c f1 be 2b ff ca 30 0d 01 ee 38 76 19 c4
            ae 12 fd 44 38 f2 03 a0 e4 e1 c4 7e c3 14 86 1f
            4e 90 87 cb 33 39 6a 68 73 e8 f9 d2 53 9a 4b 8e
            """)

        # NOTE: p value should be ignored, so testing w/ random inputs.
        engine = ScryptEngine(n=16, r=1, p=rng.randint(1, 1023))
        self.assertEqual(engine.smix(input), output)

    def test_bmix(self):
        """bmix()"""
        from passlib.crypto.scrypt._builtin import ScryptEngine
        rng = self.getRandom()

        # NOTE: bmix() call signature currently takes in list of 32*r uint32 elements,
        #       and writes to target buffer of same size.

        def check_bmix(r, input, output):
            """helper to check bmix() output against reference"""
            # NOTE: * n & p values should be ignored, so testing w/ rng inputs.
            #       * target buffer contents should be ignored, so testing w/ random inputs.
            engine = ScryptEngine(r=r, n=1 << rng.randint(1, 32), p=rng.randint(1, 1023))
            target = [rng.randint(0, 1 << 32) for _ in range((2 * r) * 16)]
            engine.bmix(input, target)
            self.assertEqual(target, list(output))

            # ScryptEngine special-cases bmix() for r=1.
            # this removes the special case patching, so we also test original bmix function.
            if r == 1:
                del engine.bmix
                target = [rng.randint(0, 1 << 32) for _ in range((2 * r) * 16)]
                engine.bmix(input, target)
                self.assertEqual(target, list(output))

        #-----------------------------------------------------------------------
        # test vector from (expired) scrypt rfc draft
        # (https://tools.ietf.org/html/draft-josefsson-scrypt-kdf-01, section 8)
        #-----------------------------------------------------------------------

        # NOTE: this pair corresponds to the first input & output pair
        #       from the test vector in test_smix(), above.
        # NOTE: original reference lists input & output as two separate 64 byte blocks.
        #       current internal representation used by bmix() uses single 2*r*16 array of uint32,
        #       combining all the B blocks into a single flat array.
        input = unpack_uint32_list(hb("""
                f7 ce 0b 65 3d 2d 72 a4 10 8c f5 ab e9 12 ff dd
                77 76 16 db bb 27 a7 0e 82 04 f3 ae 2d 0f 6f ad
                89 f6 8f 48 11 d1 e8 7b cc 3b d7 40 0a 9f fd 29
                09 4f 01 84 63 95 74 f3 9a e5 a1 31 52 17 bc d7

                89 49 91 44 72 13 bb 22 6c 25 b5 4d a8 63 70 fb
                cd 98 43 80 37 46 66 bb 8f fc b5 bf 40 c2 54 b0
                67 d2 7c 51 ce 4a d5 fe d8 29 c9 0b 50 5a 57 1b
                7f 4d 1c ad 6a 52 3c da 77 0e 67 bc ea af 7e 89
            """), 32)

        output = unpack_uint32_list(hb("""
                a4 1f 85 9c 66 08 cc 99 3b 81 ca cb 02 0c ef 05
                04 4b 21 81 a2 fd 33 7d fd 7b 1c 63 96 68 2f 29
                b4 39 31 68 e3 c9 e6 bc fe 6b c5 b7 a0 6d 96 ba
                e4 24 cc 10 2c 91 74 5c 24 ad 67 3d c7 61 8f 81

                20 ed c9 75 32 38 81 a8 05 40 f6 4c 16 2d cd 3c
                21 07 7c fe 5f 8d 5f e2 b1 a4 16 8f 95 36 78 b7
                7d 3b 3d 80 3b 60 e4 ab 92 09 96 e5 9b 4d 53 b6
                5d 2a 22 58 77 d5 ed f5 84 2c b9 f1 4e ef e4 25
            """), 32)

#        check_bmix(1, input, output)

        #-----------------------------------------------------------------------
        # custom test vector for r=2
        # used to check for bmix() breakage while optimizing implementation.
        #-----------------------------------------------------------------------

        r = 2
        input = unpack_uint32_list(seed_bytes("bmix with r=2", 128 * r))

        output = unpack_uint32_list(hb("""
            ba240854954f4585f3d0573321f10beee96f12acdc1feb498131e40512934fd7
            43e8139c17d0743c89d09ac8c3582c273c60ab85db63e410d049a9e17a42c6a1

            6c7831b11bf370266afdaff997ae1286920dea1dedf0f4a1795ba710ba9017f1
            a374400766f13ebd8969362de2d153965e9941bdde0768fa5b53e8522f116ce0

            d14774afb88f46cd919cba4bc64af7fca0ecb8732d1fc2191e0d7d1b6475cb2e
            e3db789ee478d056c4eb6c6e28b99043602dbb8dfb60c6e048bf90719da8d57d

            3c42250e40ab79a1ada6aae9299b9790f767f54f388d024a1465b30cbbe9eb89
            002d4f5c215c4259fac4d083bac5fb0b47463747d568f40bb7fa87c42f0a1dc1
            """), 32 * r)

        check_bmix(r, input, output)

        #-----------------------------------------------------------------------
        # custom test vector for r=3
        # used to check for bmix() breakage while optimizing implementation.
        #-----------------------------------------------------------------------

        r = 3
        input = unpack_uint32_list(seed_bytes("bmix with r=3", 128 * r))

        output = unpack_uint32_list(hb("""
            11ddd8cf60c61f59a6e5b128239bdc77b464101312c88bd1ccf6be6e75461b29
            7370d4770c904d0b09c402573cf409bf2db47b91ba87d5a3de469df8fb7a003c

            95a66af96dbdd88beddc8df51a2f72a6f588d67e7926e9c2b676c875da13161e
            b6262adac39e6b3003e9a6fbc8c1a6ecf1e227c03bc0af3e5f8736c339b14f84

            c7ae5b89f5e16d0faf8983551165f4bb712d97e4f81426e6b78eb63892d3ff54
            80bf406c98e479496d0f76d23d728e67d2a3d2cdbc4a932be6db36dc37c60209

            a5ca76ca2d2979f995f73fe8182eefa1ce0ba0d4fc27d5b827cb8e67edd6552f
            00a5b3ab6b371bd985a158e728011314eb77f32ade619b3162d7b5078a19886c

            06f12bc8ae8afa46489e5b0239954d5216967c928982984101e4a88bae1f60ae
            3f8a456e169a8a1c7450e7955b8a13a202382ae19d41ce8ef8b6a15eeef569a7

            20f54c48e44cb5543dda032c1a50d5ddf2919030624978704eb8db0290052a1f
            5d88989b0ef931b6befcc09e9d5162320e71e80b89862de7e2f0b6c67229b93f
            """), 32 * r)

        check_bmix(r, input, output)

        #-----------------------------------------------------------------------
        # custom test vector for r=4
        # used to check for bmix() breakage while optimizing implementation.
        #-----------------------------------------------------------------------

        r = 4
        input = unpack_uint32_list(seed_bytes("bmix with r=4", 128 * r))

        output = unpack_uint32_list(hb("""
            803fcf7362702f30ef43250f20bc6b1b8925bf5c4a0f5a14bbfd90edce545997
            3047bd81655f72588ca93f5c2f4128adaea805e0705a35e14417101fdb1c498c

            33bec6f4e5950d66098da8469f3fe633f9a17617c0ea21275185697c0e4608f7
            e6b38b7ec71704a810424637e2c296ca30d9cbf8172a71a266e0393deccf98eb

            abc430d5f144eb0805308c38522f2973b7b6a48498851e4c762874497da76b88
            b769b471fbfc144c0e8e859b2b3f5a11f51604d268c8fd28db55dff79832741a

            1ac0dfdaff10f0ada0d93d3b1f13062e4107c640c51df05f4110bdda15f51b53
            3a75bfe56489a6d8463440c78fb8c0794135e38591bdc5fa6cec96a124178a4a

            d1a976e985bfe13d2b4af51bd0fc36dd4cfc3af08efe033b2323a235205dc43d
            e57778a492153f9527338b3f6f5493a03d8015cd69737ee5096ad4cbe660b10f

            b75b1595ddc96e3748f5c9f61fba1ef1f0c51b6ceef8bbfcc34b46088652e6f7
            edab61521cbad6e69b77be30c9c97ea04a4af359dafc205c7878cc9a6c5d122f

            8d77f3cbe65ab14c3c491ef94ecb3f5d2c2dd13027ea4c3606262bb3c9ce46e7
            dc424729dc75f6e8f06096c0ad8ad4d549c42f0cad9b33cb95d10fb3cadba27c

            5f4bf0c1ac677c23ba23b64f56afc3546e62d96f96b58d7afc5029f8168cbab4
            533fd29fc83c8d2a32b81923992e4938281334e0c3694f0ee56f8ff7df7dc4ae
            """), 32 * r)

        check_bmix(r, input, output)

    def test_salsa(self):
        """salsa20()"""
        from passlib.crypto.scrypt._builtin import salsa20

        # NOTE: salsa2() currently operates on lists of 16 uint32 elements,
        #       which is what unpack_uint32_list(hb(() is for...

        #-----------------------------------------------------------------------
        # test vector from (expired) scrypt rfc draft
        # (https://tools.ietf.org/html/draft-josefsson-scrypt-kdf-01, section 7)
        #-----------------------------------------------------------------------

        # NOTE: this pair corresponds to the first input & output pair
        #       from the test vector in test_bmix(), above.

        input = unpack_uint32_list(hb("""
            7e 87 9a 21 4f 3e c9 86 7c a9 40 e6 41 71 8f 26
            ba ee 55 5b 8c 61 c1 b5 0d f8 46 11 6d cd 3b 1d
            ee 24 f3 19 df 9b 3d 85 14 12 1e 4b 5a c5 aa 32
            76 02 1d 29 09 c7 48 29 ed eb c6 8d b8 b8 c2 5e
            """))

        output = unpack_uint32_list(hb("""
            a4 1f 85 9c 66 08 cc 99 3b 81 ca cb 02 0c ef 05
            04 4b 21 81 a2 fd 33 7d fd 7b 1c 63 96 68 2f 29
            b4 39 31 68 e3 c9 e6 bc fe 6b c5 b7 a0 6d 96 ba
            e4 24 cc 10 2c 91 74 5c 24 ad 67 3d c7 61 8f 81
            """))
        self.assertEqual(salsa20(input), output)

        #-----------------------------------------------------------------------
        # custom test vector,
        # used to check for salsa20() breakage while optimizing _gen_files output.
        #-----------------------------------------------------------------------
        input = list(range(16))
        output = unpack_uint32_list(hb("""
            f518dd4fb98883e0a87954c05cab867083bb8808552810752285a05822f56c16
            9d4a2a0fd2142523d758c60b36411b682d53860514b871d27659042a5afa475d
            """))
        self.assertEqual(salsa20(input), output)

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

#=============================================================================
# test scrypt
#=============================================================================
class _CommonScryptTest(TestCase):
    """
    base class for testing various scrypt backends against same set of reference vectors.
    """
    #=============================================================================
    # class attrs
    #=============================================================================

    @classproperty
    def descriptionPrefix(cls):
        return "passlib.utils.scrypt.scrypt() <%s backend>" % cls.backend
    backend = None

    #=============================================================================
    # setup
    #=============================================================================
    def setUp(self):
        assert self.backend
        scrypt_mod._set_backend(self.backend)
        super(_CommonScryptTest, self).setUp()

    #=============================================================================
    # reference vectors
    #=============================================================================

    reference_vectors = [
        # entry format: (secret, salt, n, r, p, keylen, result)

        #------------------------------------------------------------------------
        # test vectors from scrypt whitepaper --
        # http://www.tarsnap.com/scrypt/scrypt.pdf, appendix b
        #
        # also present in (expired) scrypt rfc draft --
        # https://tools.ietf.org/html/draft-josefsson-scrypt-kdf-01, section 11
        #------------------------------------------------------------------------
        ("", "", 16, 1, 1, 64, hb("""
        77 d6 57 62 38 65 7b 20 3b 19 ca 42 c1 8a 04 97
        f1 6b 48 44 e3 07 4a e8 df df fa 3f ed e2 14 42
        fc d0 06 9d ed 09 48 f8 32 6a 75 3a 0f c8 1f 17
        e8 d3 e0 fb 2e 0d 36 28 cf 35 e2 0c 38 d1 89 06
        """)),

        ("password", "NaCl", 1024, 8, 16, 64, hb("""
        fd ba be 1c 9d 34 72 00 78 56 e7 19 0d 01 e9 fe
        7c 6a d7 cb c8 23 78 30 e7 73 76 63 4b 37 31 62
        2e af 30 d9 2e 22 a3 88 6f f1 09 27 9d 98 30 da
        c7 27 af b9 4a 83 ee 6d 83 60 cb df a2 cc 06 40
        """)),

        # NOTE: the following are skipped for all backends unless TEST_MODE="full"

        ("pleaseletmein", "SodiumChloride", 16384, 8, 1, 64, hb("""
        70 23 bd cb 3a fd 73 48 46 1c 06 cd 81 fd 38 eb
        fd a8 fb ba 90 4f 8e 3e a9 b5 43 f6 54 5d a1 f2
        d5 43 29 55 61 3f 0f cf 62 d4 97 05 24 2a 9a f9
        e6 1e 85 dc 0d 65 1e 40 df cf 01 7b 45 57 58 87
        """)),

        # NOTE: the following are always skipped for the builtin backend,
        #       (just takes too long to be worth it)

        ("pleaseletmein", "SodiumChloride", 1048576, 8, 1, 64, hb("""
        21 01 cb 9b 6a 51 1a ae ad db be 09 cf 70 f8 81
        ec 56 8d 57 4a 2f fd 4d ab e5 ee 98 20 ad aa 47
        8e 56 fd 8f 4b a5 d0 9f fa 1c 6d 92 7c 40 f4 c3
        37 30 40 49 e8 a9 52 fb cb f4 5c 6f a7 7a 41 a4
        """)),
    ]

    def test_reference_vectors(self):
        """reference vectors"""
        for secret, salt, n, r, p, keylen, result in self.reference_vectors:
            if n >= 1024 and TEST_MODE(max="default"):
                # skip large values unless we're running full test suite
                continue
            if n > 16384 and self.backend == "builtin":
                # skip largest vector for builtin, takes WAAY too long
                # (46s under pypy, ~5m under cpython)
                continue
            log.debug("scrypt reference vector: %r %r n=%r r=%r p=%r", secret, salt, n, r, p)
            self.assertEqual(scrypt_mod.scrypt(secret, salt, n, r, p, keylen), result)

    #=============================================================================
    # fuzz testing
    #=============================================================================

    _already_tested_others = None

    def test_other_backends(self):
        """compare output to other backends"""
        # only run once, since test is symetric.
        # maybe this means it should go somewhere else?
        if self._already_tested_others:
            raise self.skipTest("already run under %r backend test" % self._already_tested_others)
        self._already_tested_others = self.backend
        rng = self.getRandom()

        # get available backends
        orig = scrypt_mod.backend
        available = set(name for name in scrypt_mod.backend_values
                        if scrypt_mod._has_backend(name))
        scrypt_mod._set_backend(orig)
        available.discard(self.backend)
        if not available:
            raise self.skipTest("no other backends found")

        warnings.filterwarnings("ignore", "(?i)using builtin scrypt backend",
                                category=exc.PasslibSecurityWarning)

        # generate some random options, and cross-check output
        for _ in range(10):
            # NOTE: keeping values low due to builtin test
            secret = getrandbytes(rng, rng.randint(0, 64))
            salt = getrandbytes(rng, rng.randint(0, 64))
            n = 1 << rng.randint(1, 10)
            r = rng.randint(1, 8)
            p = rng.randint(1, 3)
            ks = rng.randint(1, 64)
            previous = None
            backends = set()
            for name in available:
                scrypt_mod._set_backend(name)
                self.assertNotIn(scrypt_mod._scrypt, backends)
                backends.add(scrypt_mod._scrypt)
                result = hexstr(scrypt_mod.scrypt(secret, salt, n, r, p, ks))
                self.assertEqual(len(result), 2*ks)
                if previous is not None:
                    self.assertEqual(result, previous,
                                     msg="%r output differs from others %r: %r" %
                                         (name, available, [secret, salt, n, r, p, ks]))

    #=============================================================================
    # test input types
    #=============================================================================
    def test_backend(self):
        """backend management"""
        # clobber backend
        scrypt_mod.backend = None
        scrypt_mod._scrypt = None
        self.assertRaises(TypeError, scrypt_mod.scrypt, 's', 's', 2, 2, 2, 16)

        # reload backend
        scrypt_mod._set_backend(self.backend)
        self.assertEqual(scrypt_mod.backend, self.backend)
        scrypt_mod.scrypt('s', 's', 2, 2, 2, 16)

        # throw error for unknown backend
        self.assertRaises(ValueError, scrypt_mod._set_backend, 'xxx')
        self.assertEqual(scrypt_mod.backend, self.backend)

    def test_secret_param(self):
        """'secret' parameter"""

        def run_scrypt(secret):
            return hexstr(scrypt_mod.scrypt(secret, "salt", 2, 2, 2, 16))

        # unicode
        TEXT = u"abc\u00defg"
        self.assertEqual(run_scrypt(TEXT), '05717106997bfe0da42cf4779a2f8bd8')

        # utf8 bytes
        TEXT_UTF8 = b'abc\xc3\x9efg'
        self.assertEqual(run_scrypt(TEXT_UTF8), '05717106997bfe0da42cf4779a2f8bd8')

        # latin1 bytes
        TEXT_LATIN1 = b'abc\xdefg'
        self.assertEqual(run_scrypt(TEXT_LATIN1), '770825d10eeaaeaf98e8a3c40f9f441d')

        # accept empty string
        self.assertEqual(run_scrypt(""), 'ca1399e5fae5d3b9578dcd2b1faff6e2')

        # reject other types
        self.assertRaises(TypeError, run_scrypt, None)
        self.assertRaises(TypeError, run_scrypt, 1)

    def test_salt_param(self):
        """'salt' parameter"""

        def run_scrypt(salt):
            return hexstr(scrypt_mod.scrypt("secret", salt, 2, 2, 2, 16))

        # unicode
        TEXT = u"abc\u00defg"
        self.assertEqual(run_scrypt(TEXT), 'a748ec0f4613929e9e5f03d1ab741d88')

        # utf8 bytes
        TEXT_UTF8 = b'abc\xc3\x9efg'
        self.assertEqual(run_scrypt(TEXT_UTF8), 'a748ec0f4613929e9e5f03d1ab741d88')

        # latin1 bytes
        TEXT_LATIN1 = b'abc\xdefg'
        self.assertEqual(run_scrypt(TEXT_LATIN1), '91d056fb76fb6e9a7d1cdfffc0a16cd1')

        # reject other types
        self.assertRaises(TypeError, run_scrypt, None)
        self.assertRaises(TypeError, run_scrypt, 1)

    def test_n_param(self):
        """'n' (rounds) parameter"""

        def run_scrypt(n):
            return hexstr(scrypt_mod.scrypt("secret", "salt", n, 2, 2, 16))

        # must be > 1, and a power of 2
        self.assertRaises(ValueError, run_scrypt, -1)
        self.assertRaises(ValueError, run_scrypt, 0)
        self.assertRaises(ValueError, run_scrypt, 1)
        self.assertEqual(run_scrypt(2), 'dacf2bca255e2870e6636fa8c8957a66')
        self.assertRaises(ValueError, run_scrypt, 3)
        self.assertRaises(ValueError, run_scrypt, 15)
        self.assertEqual(run_scrypt(16), '0272b8fc72bc54b1159340ed99425233')

    def test_r_param(self):
        """'r' (block size) parameter"""
        def run_scrypt(r, n=2, p=2):
            return hexstr(scrypt_mod.scrypt("secret", "salt", n, r, p, 16))

        # must be > 1
        self.assertRaises(ValueError, run_scrypt, -1)
        self.assertRaises(ValueError, run_scrypt, 0)
        self.assertEqual(run_scrypt(1), '3d630447d9f065363b8a79b0b3670251')
        self.assertEqual(run_scrypt(2), 'dacf2bca255e2870e6636fa8c8957a66')
        self.assertEqual(run_scrypt(5), '114f05e985a903c27237b5578e763736')

        # reject r*p >= 2**30
        self.assertRaises(ValueError, run_scrypt, (1<<30), p=1)
        self.assertRaises(ValueError, run_scrypt, (1<<30) / 2, p=2)

    def test_p_param(self):
        """'p' (parallelism) parameter"""
        def run_scrypt(p, n=2, r=2):
            return hexstr(scrypt_mod.scrypt("secret", "salt", n, r, p, 16))

        # must be > 1
        self.assertRaises(ValueError, run_scrypt, -1)
        self.assertRaises(ValueError, run_scrypt, 0)
        self.assertEqual(run_scrypt(1), 'f2960ea8b7d48231fcec1b89b784a6fa')
        self.assertEqual(run_scrypt(2), 'dacf2bca255e2870e6636fa8c8957a66')
        self.assertEqual(run_scrypt(5), '848a0eeb2b3543e7f543844d6ca79782')

        # reject r*p >= 2**30
        self.assertRaises(ValueError, run_scrypt, (1<<30), r=1)
        self.assertRaises(ValueError, run_scrypt, (1<<30) / 2, r=2)

    def test_keylen_param(self):
        """'keylen' parameter"""
        rng = self.getRandom()

        def run_scrypt(keylen):
            return hexstr(scrypt_mod.scrypt("secret", "salt", 2, 2, 2, keylen))

        # must be > 0
        self.assertRaises(ValueError, run_scrypt, -1)
        self.assertRaises(ValueError, run_scrypt, 0)
        self.assertEqual(run_scrypt(1), 'da')

        # pick random value
        ksize = rng.randint(0, 1 << 10)
        self.assertEqual(len(run_scrypt(ksize)), 2*ksize) # 2 hex chars per output

        # one more than upper bound
        self.assertRaises(ValueError, run_scrypt, ((2**32) - 1) * 32 + 1)

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


#-----------------------------------------------------------------------
# check what backends 'should' be available
#-----------------------------------------------------------------------

def _can_import_cffi_scrypt():
    try:
        import scrypt
    except ImportError as err:
        if "scrypt" in str(err):
            return False
        raise
    return True

has_cffi_scrypt = _can_import_cffi_scrypt()


def _can_import_stdlib_scrypt():
    try:
        from hashlib import scrypt
        return True
    except ImportError:
        return False

has_stdlib_scrypt = _can_import_stdlib_scrypt()

#-----------------------------------------------------------------------
# test individual backends
#-----------------------------------------------------------------------

# NOTE: builtin version runs VERY slow (except under PyPy, where it's only 11x slower),
#       so skipping under quick test mode.
@skipUnless(PYPY or TEST_MODE(min="default"), "skipped under current test mode")
class BuiltinScryptTest(_CommonScryptTest):
    backend = "builtin"

    def setUp(self):
        super(BuiltinScryptTest, self).setUp()
        warnings.filterwarnings("ignore", "(?i)using builtin scrypt backend",
                                category=exc.PasslibSecurityWarning)

    def test_missing_backend(self):
        """backend management -- missing backend"""
        if has_stdlib_scrypt or has_cffi_scrypt:
            raise self.skipTest("non-builtin backend is present")
        self.assertRaises(exc.MissingBackendError, scrypt_mod._set_backend, 'scrypt')


@skipUnless(has_cffi_scrypt, "'scrypt' package not found")
class ScryptPackageTest(_CommonScryptTest):
    backend = "scrypt"

    def test_default_backend(self):
        """backend management -- default backend"""
        if has_stdlib_scrypt:
            raise self.skipTest("higher priority backend present")
        scrypt_mod._set_backend("default")
        self.assertEqual(scrypt_mod.backend, "scrypt")


@skipUnless(has_stdlib_scrypt, "'hashlib.scrypt()' not found")
class StdlibScryptTest(_CommonScryptTest):
    backend = "stdlib"

    def test_default_backend(self):
        """backend management -- default backend"""
        scrypt_mod._set_backend("default")
        self.assertEqual(scrypt_mod.backend, "stdlib")

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