comparison MoinMoin/util/crypto.py @ 174:e8f61cbd661b

modularize crypto/random stuff, move it to MoinMoin.util.crypto, pw change bugfix on password change, the new password was not saved to the profile User / crypto code: minor optimizations / refactorings crypto module contents: password hashing/encryption, validation, pw hash upgrades: * pw_hash = crypt_password(password) * is_valid = valid_password(password, pw_hash) * upgraded_pw_hash = upgrade_password(password, pw_hash) password recovery: * key, token = generate_token() * is_valid = valid_token(key, token) random strings: * rs = random_string(length, chars) compute ascii cache keys: * key = cache_key(**kw)
author Thomas Waldmann <tw AT waldmann-edv DOT de>
date Thu, 07 Apr 2011 20:56:35 +0200
parents
children 316c839a6f62
comparison
equal deleted inserted replaced
173:89f50aed143f 174:e8f61cbd661b
1 # Copyright: 2000-2004 Juergen Hermann <jh@web.de>
2 # Copyright: 2003-2011 MoinMoin:ThomasWaldmann
3 # Copyright: 2007 MoinMoin:JohannesBerg
4 # Copyright: 2007 MoinMoin:HeinrichWendel
5 # Copyright: 2008 MoinMoin:ChristopherDenter
6 # Copyright: 2010 MoinMoin:DiogenesAugusto
7 # License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
8
9 """
10 MoinMoin - Cryptographic and random functions
11 """
12
13 from __future__ import absolute_import, division
14
15 import base64
16 import hashlib
17 import hmac
18 import random
19
20 try:
21 import crypt
22 except ImportError:
23 crypt = None
24
25 from . import md5crypt
26
27 # random stuff
28
29 def random_string(length, allowed_chars=None):
30 """
31 Generate a random string with given length consisting of the given characters.
32
33 :param length: length of the string
34 :param allowed_chars: string with allowed characters or None
35 to indicate all 256 byte values should be used
36 :returns: random string
37 """
38 if allowed_chars is None:
39 s = ''.join([chr(random.randint(0, 255)) for dummy in xrange(length)])
40 else:
41 s = ''.join([random.choice(allowed_chars) for dummy in xrange(length)])
42 return s
43
44
45 # password stuff
46
47 def crypt_password(password, salt=None):
48 """
49 Crypt/Hash a cleartext password
50
51 :param password: cleartext password [unicode]
52 :param salt: salt for the password [str] or None to generate a random salt
53 :rtype: str
54 :returns: the SSHA256 password hash
55 """
56 password = password.encode('utf-8')
57 if salt is None:
58 salt = random_string(32)
59 assert isinstance(salt, str)
60 h = hashlib.new('sha256', password)
61 h.update(salt)
62 return '{SSHA256}' + base64.encodestring(h.digest() + salt).rstrip()
63
64
65 def upgrade_password(password, pw_hash):
66 """
67 Upgrade a password to a better hash, if needed
68
69 :param password: cleartext password [unicode]
70 :param pw_hash: password hash (with hash type prefix)
71 :rtype: str
72 :returns: new password hash (or None, if unchanged)
73 """
74 if not pw_hash.startswith('{SSHA256}'):
75 # pw_hash using some old hash method, upgrade to better method
76 return crypt_password(password)
77
78
79 def valid_password(password, pw_hash):
80 """
81 Validate a user password.
82
83 :param password: cleartext password to verify [unicode]
84 :param pw_hash: password hash (with hash type prefix)
85 :rtype: bool
86 :returns: password is valid
87 """
88 # encode password
89 pw_utf8 = password.encode('utf-8')
90
91 methods = ['{SSHA256}', '{SSHA}', '{SHA}', '{APR1}', '{MD5}', ]
92 if crypt:
93 # we have the crypt module
94 methods.append('{DES}')
95
96 for method in methods:
97 if pw_hash.startswith(method):
98 d = pw_hash[len(method):]
99 if method == '{SSHA256}':
100 ph = base64.decodestring(d)
101 # ph is of the form "<hash><salt>"
102 salt = ph[32:]
103 h = hashlib.new('sha256', pw_utf8)
104 h.update(salt)
105 enc = base64.encodestring(h.digest() + salt).rstrip()
106 elif method == '{SSHA}':
107 ph = base64.decodestring(d)
108 # ph is of the form "<hash><salt>"
109 salt = ph[20:]
110 h = hashlib.new('sha1', pw_utf8)
111 h.update(salt)
112 enc = base64.encodestring(h.digest() + salt).rstrip()
113 elif method == '{SHA}':
114 h = hashlib.new('sha1', pw_utf8)
115 enc = base64.encodestring(h.digest()).rstrip()
116 elif method == '{APR1}':
117 # d is of the form "$apr1$<salt>$<hash>"
118 salt = d.split('$')[2]
119 enc = md5crypt.apache_md5_crypt(pw_utf8, salt.encode('ascii'))
120 elif method == '{MD5}':
121 # d is of the form "$1$<salt>$<hash>"
122 salt = d.split('$')[2]
123 enc = md5crypt.unix_md5_crypt(pw_utf8, salt.encode('ascii'))
124 elif method == '{DES}':
125 # d is 2 characters salt + 11 characters hash
126 salt = d[:2]
127 enc = crypt.crypt(pw_utf8, salt.encode('ascii'))
128 else:
129 raise ValueError("missing password hash method %s handler" % method)
130
131 return pw_hash == method + enc
132 else:
133 idx = pw_hash.index('}') + 1
134 raise ValueError("unsupported password hash method %r" % pw_hash[:idx])
135
136
137 # password recovery token
138
139 def generate_token(key=None, stamp=None):
140 """
141 generate a pair of a secret key and a crypto token.
142
143 you can use this to implement a password recovery functionality by
144 calling generate_token() and transmitting the returned token to the
145 (correct) user (e.g. by email) and storing the returned (secret) key
146 into the user's profile on the server side.
147
148 after the user received the token, he returns to the wiki, gives his
149 user name or email address and the token he received. read the (secret)
150 key from the user profile and call valid_token(key, token) to verify
151 if the token is valid. if it is, consider the user authenticated, remove
152 the secret key from his profile and let him reset his password.
153
154 :param key: give it to recompute some specific token for verification
155 :param stamp: give it to recompute some specific token for verification
156 :rtype: 2-tuple
157 :returns: key, token
158 """
159 if key is None:
160 key = random_string(64, "abcdefghijklmnopqrstuvwxyz0123456789")
161 if stamp is None:
162 stamp = int(time.time())
163 h = hmac.new(str(key), str(stamp), digestmod=hashlib.sha1).hexdigest()
164 token = stamp + '-' + h
165 return key, token
166
167
168 def valid_token(key, token, timeout=12*60*60):
169 """
170 check if token is valid with respect to the secret key,
171 the token must not be older than timeout seconds.
172
173 :param key: give the secret key to verify the token
174 :param token: the token to verify
175 :rtype: bool
176 :returns: token is valid and not timed out
177 """
178 parts = token.split('-')
179 if len(parts) != 2:
180 return False
181 try:
182 stamp = int(parts[0])
183 except ValueError:
184 return False
185 if stamp + timeout < time.time():
186 return False
187 expected_token = generate_token(key, stamp)[1]
188 return token == expected_token
189
190
191 # miscellaneous
192
193 def cache_key(**kw):
194 """
195 Calculate a cache key (ascii only)
196
197 Important key properties:
198
199 * The key must be different for different <kw>.
200 * Key is pure ascii
201
202 :param kw: keys/values to compute cache key from
203 """
204 return hashlib.md5(repr(kw)).hexdigest()
205