1 #
2 # Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
3 # This module is free software, and you may redistribute it and/or modify
4 # under the same terms as Python, so long as this copyright message and
5 # disclaimer are retained in their original form.
6 #
7 # IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
8 # DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
9 # OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
10 # POSSIBILITY OF SUCH DAMAGE.
11 #
12 # BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
13 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
14 # FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
15 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
16 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
17 #
18 # $Id: password.py,v 1.15 2005-12-25 15:38:40 a1s Exp $
20 """Password handling (encoding, decoding).
21 """
22 __docformat__ = 'restructuredtext'
24 import re, string, random
25 from base64 import b64encode, b64decode
26 from roundup.anypy.hashlib_ import md5, sha1, shamodule
27 try:
28 import crypt
29 except ImportError:
30 crypt = None
32 _bempty = ""
33 _bjoin = _bempty.join
35 def getrandbytes(count):
36 return _bjoin(chr(random.randint(0,255)) for i in xrange(count))
38 #NOTE: PBKDF2 hash is using this variant of base64 to minimize encoding size,
39 # and have charset that's compatible w/ unix crypt variants
40 def h64encode(data):
41 """encode using variant of base64"""
42 return b64encode(data, "./").strip("=\n")
44 def h64decode(data):
45 """decode using variant of base64"""
46 off = len(data) % 4
47 if off == 0:
48 return b64decode(data, "./")
49 elif off == 1:
50 raise ValueError("invalid bas64 input")
51 elif off == 2:
52 return b64decode(data + "==", "./")
53 else:
54 return b64decode(data + "=", "./")
56 try:
57 from M2Crypto.EVP import pbkdf2 as _pbkdf2
58 except ImportError:
59 #no m2crypto - make our own pbkdf2 function
60 from struct import pack
61 from hmac import HMAC
63 def xor_bytes(left, right):
64 "perform bitwise-xor of two byte-strings"
65 return _bjoin(chr(ord(l) ^ ord(r)) for l, r in zip(left, right))
67 def _pbkdf2(password, salt, rounds, keylen):
68 digest_size = 20 # sha1 generates 20-byte blocks
69 total_blocks = int((keylen+digest_size-1)/digest_size)
70 hmac_template = HMAC(password, None, shamodule)
71 out = _bempty
72 for i in xrange(1, total_blocks+1):
73 hmac = hmac_template.copy()
74 hmac.update(salt + pack(">L",i))
75 block = tmp = hmac.digest()
76 for j in xrange(rounds-1):
77 hmac = hmac_template.copy()
78 hmac.update(tmp)
79 tmp = hmac.digest()
80 #TODO: need to speed up this call
81 block = xor_bytes(block, tmp)
82 out += block
83 return out[:keylen]
85 def pbkdf2(password, salt, rounds, keylen):
86 """pkcs#5 password-based key derivation v2.0
88 :arg password: passphrase to use to generate key (if unicode, converted to utf-8)
89 :arg salt: salt string to use when generating key (if unicode, converted to utf-8)
90 :param rounds: number of rounds to use to generate key
91 :arg keylen: number of bytes to generate
93 If M2Crypto is present, uses it's implementation as backend.
95 :returns:
96 raw bytes of generated key
97 """
98 if isinstance(password, unicode):
99 password = password.encode("utf-8")
100 if isinstance(salt, unicode):
101 salt = salt.encode("utf-8")
102 if keylen > 40:
103 #NOTE: pbkdf2 allows up to (2**31-1)*20 bytes,
104 # but m2crypto has issues on some platforms above 40,
105 # and such sizes aren't needed for a password hash anyways...
106 raise ValueError, "key length too large"
107 if rounds < 1:
108 raise ValueError, "rounds must be positive number"
109 return _pbkdf2(password, salt, rounds, keylen)
111 class PasswordValueError(ValueError):
112 """ The password value is not valid """
113 pass
115 def pbkdf2_unpack(pbkdf2):
116 """ unpack pbkdf2 encrypted password into parts,
117 assume it has format "{rounds}${salt}${digest}
118 """
119 if isinstance(pbkdf2, unicode):
120 pbkdf2 = pbkdf2.encode("ascii")
121 try:
122 rounds, salt, digest = pbkdf2.split("$")
123 except ValueError:
124 raise PasswordValueError, "invalid PBKDF2 hash (wrong number of separators)"
125 if rounds.startswith("0"):
126 raise PasswordValueError, "invalid PBKDF2 hash (zero-padded rounds)"
127 try:
128 rounds = int(rounds)
129 except ValueError:
130 raise PasswordValueError, "invalid PBKDF2 hash (invalid rounds)"
131 raw_salt = h64decode(salt)
132 return rounds, salt, raw_salt, digest
134 def encodePassword(plaintext, scheme, other=None, config=None):
135 """Encrypt the plaintext password.
136 """
137 if plaintext is None:
138 plaintext = ""
139 if scheme == "PBKDF2":
140 if other:
141 rounds, salt, raw_salt, digest = pbkdf2_unpack(other)
142 else:
143 raw_salt = getrandbytes(20)
144 salt = h64encode(raw_salt)
145 if config:
146 rounds = config.PASSWORD_PBKDF2_DEFAULT_ROUNDS
147 else:
148 rounds = 10000
149 if rounds < 1000:
150 raise PasswordValueError, "invalid PBKDF2 hash (rounds too low)"
151 raw_digest = pbkdf2(plaintext, raw_salt, rounds, 20)
152 return "%d$%s$%s" % (rounds, salt, h64encode(raw_digest))
153 elif scheme == 'SHA':
154 s = sha1(plaintext).hexdigest()
155 elif scheme == 'MD5':
156 s = md5(plaintext).hexdigest()
157 elif scheme == 'crypt' and crypt is not None:
158 if other is not None:
159 salt = other
160 else:
161 saltchars = './0123456789'+string.letters
162 salt = random.choice(saltchars) + random.choice(saltchars)
163 s = crypt.crypt(plaintext, salt)
164 elif scheme == 'plaintext':
165 s = plaintext
166 else:
167 raise PasswordValueError, 'unknown encryption scheme %r'%scheme
168 return s
170 def generatePassword(length=8):
171 chars = string.letters+string.digits
172 return ''.join([random.choice(chars) for x in range(length)])
174 class JournalPassword:
175 """ Password dummy instance intended for journal operation.
176 We do not store passwords in the journal any longer. The dummy
177 version only reads the encryption scheme from the given
178 encrypted password.
179 """
180 default_scheme = 'PBKDF2' # new encryptions use this scheme
181 pwre = re.compile(r'{(\w+)}(.+)')
183 def __init__ (self, encrypted=''):
184 if isinstance(encrypted, self.__class__):
185 self.scheme = encrypted.scheme or self.default_scheme
186 else:
187 m = self.pwre.match(encrypted)
188 if m:
189 self.scheme = m.group(1)
190 else:
191 self.scheme = self.default_scheme
192 self.password = ''
194 def dummystr(self):
195 """ return dummy string to store in journal
196 - reports scheme, but nothing else
197 """
198 return "{%s}*encrypted*" % (self.scheme,)
200 __str__ = dummystr
202 def __cmp__(self, other):
203 """Compare this password against another password."""
204 # check to see if we're comparing instances
205 if isinstance(other, self.__class__):
206 if self.scheme != other.scheme:
207 return cmp(self.scheme, other.scheme)
208 return cmp(self.password, other.password)
210 # assume password is plaintext
211 if self.password is None:
212 raise ValueError, 'Password not set'
213 return cmp(self.password, encodePassword(other, self.scheme,
214 self.password or None))
216 class Password(JournalPassword):
217 """The class encapsulates a Password property type value in the database.
219 The encoding of the password is one if None, 'SHA', 'MD5' or 'plaintext'.
220 The encodePassword function is used to actually encode the password from
221 plaintext. The None encoding is used in legacy databases where no
222 encoding scheme is identified.
224 The scheme is stored with the encoded data in the database:
225 {scheme}data
227 Example usage:
228 >>> p = Password('sekrit')
229 >>> p == 'sekrit'
230 1
231 >>> p != 'not sekrit'
232 1
233 >>> 'sekrit' == p
234 1
235 >>> 'not sekrit' != p
236 1
237 """
238 #TODO: code to migrate from old password schemes.
240 deprecated_schemes = ["SHA", "MD5", "crypt", "plaintext"]
241 known_schemes = ["PBKDF2"] + deprecated_schemes
243 def __init__(self, plaintext=None, scheme=None, encrypted=None, strict=False, config=None):
244 """Call setPassword if plaintext is not None."""
245 if scheme is None:
246 scheme = self.default_scheme
247 if plaintext is not None:
248 self.setPassword (plaintext, scheme, config=config)
249 elif encrypted is not None:
250 self.unpack(encrypted, scheme, strict=strict, config=config)
251 else:
252 self.scheme = self.default_scheme
253 self.password = None
254 self.plaintext = None
256 def needs_migration(self):
257 """ Password has insecure scheme or other insecure parameters
258 and needs migration to new password scheme
259 """
260 if self.scheme in self.deprecated_schemes:
261 return True
262 rounds, salt, raw_salt, digest = pbkdf2_unpack(self.password)
263 if rounds < 1000:
264 return True
265 return False
267 def unpack(self, encrypted, scheme=None, strict=False, config=None):
268 """Set the password info from the scheme:<encryted info> string
269 (the inverse of __str__)
270 """
271 m = self.pwre.match(encrypted)
272 if m:
273 self.scheme = m.group(1)
274 self.password = m.group(2)
275 self.plaintext = None
276 else:
277 # currently plaintext - encrypt
278 self.setPassword(encrypted, scheme, config=config)
279 if strict and self.scheme not in self.known_schemes:
280 raise PasswordValueError, "unknown encryption scheme: %r" % (self.scheme,)
282 def setPassword(self, plaintext, scheme=None, config=None):
283 """Sets encrypts plaintext."""
284 if scheme is None:
285 scheme = self.default_scheme
286 self.scheme = scheme
287 self.password = encodePassword(plaintext, scheme, config=config)
288 self.plaintext = plaintext
290 def __str__(self):
291 """Stringify the encrypted password for database storage."""
292 if self.password is None:
293 raise ValueError, 'Password not set'
294 return '{%s}%s'%(self.scheme, self.password)
296 def test():
297 # SHA
298 p = Password('sekrit')
299 assert p == 'sekrit'
300 assert p != 'not sekrit'
301 assert 'sekrit' == p
302 assert 'not sekrit' != p
304 # MD5
305 p = Password('sekrit', 'MD5')
306 assert p == 'sekrit'
307 assert p != 'not sekrit'
308 assert 'sekrit' == p
309 assert 'not sekrit' != p
311 # crypt
312 p = Password('sekrit', 'crypt')
313 assert p == 'sekrit'
314 assert p != 'not sekrit'
315 assert 'sekrit' == p
316 assert 'not sekrit' != p
318 # PBKDF2 - low level function
319 from binascii import unhexlify
320 k = pbkdf2("password", "ATHENA.MIT.EDUraeburn", 1200, 32)
321 assert k == unhexlify("5c08eb61fdf71e4e4ec3cf6ba1f5512ba7e52ddbc5e5142f708a31e2e62b1e13")
323 # PBKDF2 - hash function
324 h = "5000$7BvbBq.EZzz/O0HuwX3iP.nAG3s$g3oPnFFaga2BJaX5PoPRljl4XIE"
325 assert encodePassword("sekrit", "PBKDF2", h) == h
327 # PBKDF2 - high level integration
328 p = Password('sekrit', 'PBKDF2')
329 assert p == 'sekrit'
330 assert p != 'not sekrit'
331 assert 'sekrit' == p
332 assert 'not sekrit' != p
334 if __name__ == '__main__':
335 test()
337 # vim: set filetype=python sts=4 sw=4 et si :