eab300775b9c8a7869904c08aebc21d528f69160
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
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
62 try:
63 from hashlib import sha1
64 except ImportError:
65 from sha import new as sha1
67 def xor_bytes(left, right):
68 "perform bitwise-xor of two byte-strings"
69 return _bjoin(chr(ord(l) ^ ord(r)) for l, r in zip(left, right))
71 def _pbkdf2(password, salt, rounds, keylen):
72 digest_size = 20 # sha1 generates 20-byte blocks
73 total_blocks = int((keylen+digest_size-1)/digest_size)
74 hmac_template = HMAC(password, None, sha1)
75 out = _bempty
76 for i in xrange(1, total_blocks+1):
77 hmac = hmac_template.copy()
78 hmac.update(salt + pack(">L",i))
79 block = tmp = hmac.digest()
80 for j in xrange(rounds-1):
81 hmac = hmac_template.copy()
82 hmac.update(tmp)
83 tmp = hmac.digest()
84 #TODO: need to speed up this call
85 block = xor_bytes(block, tmp)
86 out += block
87 return out[:keylen]
89 def pbkdf2(password, salt, rounds, keylen):
90 """pkcs#5 password-based key derivation v2.0
92 :arg password: passphrase to use to generate key (if unicode, converted to utf-8)
93 :arg salt: salt string to use when generating key (if unicode, converted to utf-8)
94 :param rounds: number of rounds to use to generate key
95 :arg keylen: number of bytes to generate
97 If M2Crypto is present, uses it's implementation as backend.
99 :returns:
100 raw bytes of generated key
101 """
102 if isinstance(password, unicode):
103 password = password.encode("utf-8")
104 if isinstance(salt, unicode):
105 salt = salt.encode("utf-8")
106 if keylen > 40:
107 #NOTE: pbkdf2 allows up to (2**31-1)*20 bytes,
108 # but m2crypto has issues on some platforms above 40,
109 # and such sizes aren't needed for a password hash anyways...
110 raise ValueError, "key length too large"
111 if rounds < 1:
112 raise ValueError, "rounds must be positive number"
113 return _pbkdf2(password, salt, rounds, keylen)
115 class PasswordValueError(ValueError):
116 """ The password value is not valid """
117 pass
119 def pbkdf2_unpack(pbkdf2):
120 """ unpack pbkdf2 encrypted password into parts,
121 assume it has format "{rounds}${salt}${digest}
122 """
123 if isinstance(pbkdf2, unicode):
124 pbkdf2 = pbkdf2.encode("ascii")
125 try:
126 rounds, salt, digest = pbkdf2.split("$")
127 except ValueError:
128 raise PasswordValueError, "invalid PBKDF2 hash (wrong number of separators)"
129 if rounds.startswith("0"):
130 raise PasswordValueError, "invalid PBKDF2 hash (zero-padded rounds)"
131 try:
132 rounds = int(rounds)
133 except ValueError:
134 raise PasswordValueError, "invalid PBKDF2 hash (invalid rounds)"
135 raw_salt = h64decode(salt)
136 return rounds, salt, raw_salt, digest
138 def encodePassword(plaintext, scheme, other=None, config=None):
139 """Encrypt the plaintext password.
140 """
141 if plaintext is None:
142 plaintext = ""
143 if scheme == "PBKDF2":
144 if other:
145 rounds, salt, raw_salt, digest = pbkdf2_unpack(other)
146 else:
147 raw_salt = getrandbytes(20)
148 salt = h64encode(raw_salt)
149 if config:
150 rounds = config.PASSWORD_PBKDF2_DEFAULT_ROUNDS
151 else:
152 rounds = 10000
153 if rounds < 1000:
154 raise PasswordValueError, "invalid PBKDF2 hash (rounds too low)"
155 raw_digest = pbkdf2(plaintext, raw_salt, rounds, 20)
156 return "%d$%s$%s" % (rounds, salt, h64encode(raw_digest))
157 elif scheme == 'SHA':
158 s = sha1(plaintext).hexdigest()
159 elif scheme == 'MD5':
160 s = md5(plaintext).hexdigest()
161 elif scheme == 'crypt' and crypt is not None:
162 if other is not None:
163 salt = other
164 else:
165 saltchars = './0123456789'+string.letters
166 salt = random.choice(saltchars) + random.choice(saltchars)
167 s = crypt.crypt(plaintext, salt)
168 elif scheme == 'plaintext':
169 s = plaintext
170 else:
171 raise PasswordValueError, 'unknown encryption scheme %r'%scheme
172 return s
174 def generatePassword(length=8):
175 chars = string.letters+string.digits
176 return ''.join([random.choice(chars) for x in range(length)])
178 class JournalPassword:
179 """ Password dummy instance intended for journal operation.
180 We do not store passwords in the journal any longer. The dummy
181 version only reads the encryption scheme from the given
182 encrypted password.
183 """
184 default_scheme = 'PBKDF2' # new encryptions use this scheme
185 pwre = re.compile(r'{(\w+)}(.+)')
187 def __init__ (self, encrypted=''):
188 if isinstance(encrypted, self.__class__):
189 self.scheme = encrypted.scheme or self.default_scheme
190 else:
191 m = self.pwre.match(encrypted)
192 if m:
193 self.scheme = m.group(1)
194 else:
195 self.scheme = self.default_scheme
196 self.password = ''
198 def dummystr(self):
199 """ return dummy string to store in journal
200 - reports scheme, but nothing else
201 """
202 return "{%s}*encrypted*" % (self.scheme,)
204 __str__ = dummystr
206 def __cmp__(self, other):
207 """Compare this password against another password."""
208 # check to see if we're comparing instances
209 if isinstance(other, self.__class__):
210 if self.scheme != other.scheme:
211 return cmp(self.scheme, other.scheme)
212 return cmp(self.password, other.password)
214 # assume password is plaintext
215 if self.password is None:
216 raise ValueError, 'Password not set'
217 return cmp(self.password, encodePassword(other, self.scheme,
218 self.password or None))
220 class Password(JournalPassword):
221 """The class encapsulates a Password property type value in the database.
223 The encoding of the password is one if None, 'SHA', 'MD5' or 'plaintext'.
224 The encodePassword function is used to actually encode the password from
225 plaintext. The None encoding is used in legacy databases where no
226 encoding scheme is identified.
228 The scheme is stored with the encoded data in the database:
229 {scheme}data
231 Example usage:
232 >>> p = Password('sekrit')
233 >>> p == 'sekrit'
234 1
235 >>> p != 'not sekrit'
236 1
237 >>> 'sekrit' == p
238 1
239 >>> 'not sekrit' != p
240 1
241 """
242 #TODO: code to migrate from old password schemes.
244 deprecated_schemes = ["SHA", "MD5", "crypt", "plaintext"]
245 known_schemes = ["PBKDF2"] + deprecated_schemes
247 def __init__(self, plaintext=None, scheme=None, encrypted=None, strict=False, config=None):
248 """Call setPassword if plaintext is not None."""
249 if scheme is None:
250 scheme = self.default_scheme
251 if plaintext is not None:
252 self.setPassword (plaintext, scheme, config=config)
253 elif encrypted is not None:
254 self.unpack(encrypted, scheme, strict=strict, config=config)
255 else:
256 self.scheme = self.default_scheme
257 self.password = None
258 self.plaintext = None
260 def needs_migration(self):
261 """ Password has insecure scheme or other insecure parameters
262 and needs migration to new password scheme
263 """
264 if self.scheme in self.deprecated_schemes:
265 return True
266 rounds, salt, raw_salt, digest = pbkdf2_unpack(self.password)
267 if rounds < 1000:
268 return True
269 return False
271 def unpack(self, encrypted, scheme=None, strict=False, config=None):
272 """Set the password info from the scheme:<encryted info> string
273 (the inverse of __str__)
274 """
275 m = self.pwre.match(encrypted)
276 if m:
277 self.scheme = m.group(1)
278 self.password = m.group(2)
279 self.plaintext = None
280 else:
281 # currently plaintext - encrypt
282 self.setPassword(encrypted, scheme, config=config)
283 if strict and self.scheme not in self.known_schemes:
284 raise PasswordValueError, "unknown encryption scheme: %r" % (self.scheme,)
286 def setPassword(self, plaintext, scheme=None, config=None):
287 """Sets encrypts plaintext."""
288 if scheme is None:
289 scheme = self.default_scheme
290 self.scheme = scheme
291 self.password = encodePassword(plaintext, scheme, config=config)
292 self.plaintext = plaintext
294 def __str__(self):
295 """Stringify the encrypted password for database storage."""
296 if self.password is None:
297 raise ValueError, 'Password not set'
298 return '{%s}%s'%(self.scheme, self.password)
300 def test():
301 # SHA
302 p = Password('sekrit')
303 assert p == 'sekrit'
304 assert p != 'not sekrit'
305 assert 'sekrit' == p
306 assert 'not sekrit' != p
308 # MD5
309 p = Password('sekrit', 'MD5')
310 assert p == 'sekrit'
311 assert p != 'not sekrit'
312 assert 'sekrit' == p
313 assert 'not sekrit' != p
315 # crypt
316 p = Password('sekrit', 'crypt')
317 assert p == 'sekrit'
318 assert p != 'not sekrit'
319 assert 'sekrit' == p
320 assert 'not sekrit' != p
322 # PBKDF2 - low level function
323 from binascii import unhexlify
324 k = pbkdf2("password", "ATHENA.MIT.EDUraeburn", 1200, 32)
325 assert k == unhexlify("5c08eb61fdf71e4e4ec3cf6ba1f5512ba7e52ddbc5e5142f708a31e2e62b1e13")
327 # PBKDF2 - hash function
328 h = "5000$7BvbBq.EZzz/O0HuwX3iP.nAG3s$g3oPnFFaga2BJaX5PoPRljl4XIE"
329 assert encodePassword("sekrit", "PBKDF2", h) == h
331 # PBKDF2 - high level integration
332 p = Password('sekrit', 'PBKDF2')
333 assert p == 'sekrit'
334 assert p != 'not sekrit'
335 assert 'sekrit' == p
336 assert 'not sekrit' != p
338 if __name__ == '__main__':
339 test()
341 # vim: set filetype=python sts=4 sw=4 et si :