summaryrefslogtreecommitdiff
path: root/passlib/utils/pbkdf2.py
blob: ad6f0877fc830dea01e2fa2b1057732bba12f57b (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
"""passlib.pbkdf2 - PBKDF2 support

"""
#=================================================================================
#imports
#=================================================================================
#core
from binascii import unhexlify
from cStringIO import StringIO
import hashlib
import hmac
import logging; log = logging.getLogger(__name__)
import re
from struct import pack
from warnings import warn
#site
try:
    from M2Crypto import EVP as _EVP
except ImportError:
    _EVP = None
#pkg
from passlib.utils import xor_bytes
#local
__all__ = [
    "hmac_sha1",
    "pbkdf2",
]

#=================================================================================
#hmac sha1 support
#=================================================================================
def hmac_sha1(key, msg):
    "perform raw hmac-sha1 of a message"
    return hmac.new(key, msg, hashlib.sha1).digest()

if _EVP:
    #default *should* be sha1, which saves us a wrapper function, but might as well check.
    try:
        result = _EVP.hmac('x','y')
    except ValueError: #pragma: no cover
        #this is probably not a good sign if it happens.
        warn("PassLib: M2Crypt.EVP.hmac() unexpected threw value error during passlib startup test")
    else:
        if result == ',\x1cb\xe0H\xa5\x82M\xfb>\xd6\x98\xef\x8e\xf9oQ\x85\xa3i':
            hmac_sha1 = _EVP.hmac

#=================================================================================
#backend
#=================================================================================
MAX_BLOCKS = 0xffffffffL #2**32-1

def _resolve_prf(prf):
    "resolve prf string or callable -> func & digest_size"
    if isinstance(prf, str):
        if prf.startswith("hmac-"):
            digest = prf[5:]

            #check if m2crypto is present and supports requested digest
            if _EVP:
                try:
                    result = _EVP.hmac('x', 'y', digest)
                except ValueError:
                    pass
                else:
                    #it does. so use M2Crypto's hmac & digest code
                    hmac_const = _EVP.hmac
                    def encode_block(key, msg):
                        return hmac_const(key, msg, digest)
                    digest_size = len(result)
                    return encode_block, digest_size

            #fall back to stdlib implementation
            digest_const = getattr(hashlib, digest, None)
            if not digest_const:
                raise ValueError, "unknown hash algorithm: %r" % (digest,)
            digest_size = digest_const().digest_size
            hmac_const = hmac.new
            def encode_block(key, msg):
                return hmac_const(key, msg, digest_const).digest()
            return encode_block, digest_size

        else:
            raise ValueError, "unknown prf algorithm: %r" % (prf,)

    elif callable(prf):
        #assume it's a callable, use it directly
        digest_size = len(prf('',''))
        return prf, digest_size

    else:
        raise TypeError, "prf must be string or callable"

def pbkdf2(secret, salt, rounds, keylen, prf="hmac-sha1"):
    """pkcs#5 password-based key derivation v2.0

    :arg secret: passphrase to use to generate key
    :arg salt: salt string to use when generating key
    :param rounds: number of rounds to use to generate key
    :arg keylen: number of bytes to generate
    :param prf:
        psuedo-random function to use for key strengthening.
        should be a callable of the form ``prf(secret, plaintext) -> ciphertext``,
        or a string ``hmac-xxx`` where ``xxx`` is the name
        of a hash function recognized by :func:`M2Crypto.EVP.hmac` (if present),
        or :func:`hashlib.new`.
        Defaults to ``hmac-sha1``, the only prf defined
        by the PBKDF2 specification.

    This function attempts to use M2Crypto as a backend
    if available and if the digest is a string supported
    by M2Crypto. Otherwise it falls back to a software implementation.

    :returns:
        raw bytes of generated key
    """

    #prepare secret
    if isinstance(secret, unicode):
        secret = secret.encode("utf-8")
    elif not isinstance(secret, str):
        raise TypeError("secret must be str or unicode")

    #prepare salt
    if isinstance(salt, unicode):
        salt = salt.encode("utf-8")
    elif not isinstance(salt, str):
        raise TypeError("salt must be str or unicode")

    #preprare rounds
    if not isinstance(rounds, (int, long)):
        raise TypeError("rounds must be an integer")
    if rounds < 1:
        raise ValueError("rounds must be at least 1")

    #special case for m2crypto + hmac-sha1
    if prf == "hmac-sha1" and _EVP:
        try:
            return _EVP.pbkdf2(secret, salt, rounds, keylen)
        except OverflowError:
            raise ValueError, "key length too long"

    #resolve prf
    encode_block, digest_size = _resolve_prf(prf)

    #figure out how many blocks we'll need
    bcount = (keylen+digest_size-1)//digest_size
    if bcount >= MAX_BLOCKS:
        raise ValueError, "key length to long"

    #build up key from blocks
    out = StringIO()
    write = out.write
    for i in xrange(1,bcount+1):
        block = tmp = encode_block(secret, salt + pack(">L", i))
        #NOTE: could potentially unroll this loop somewhat for speed,
        # or find some faster way to accumulate & xor tmp values together
        for j in xrange(rounds-1):
            tmp = encode_block(secret, tmp)
            block = xor_bytes(block, tmp)
        write(block)

    #and done
    return out.getvalue()[:keylen]

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