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 encodePassword(plaintext, scheme, other=None):
120 """Encrypt the plaintext password.
121 """
122 if plaintext is None:
123 plaintext = ""
124 if scheme == "PBKDF2":
125 if other:
126 #assume it has format "{rounds}${salt}${digest}"
127 if isinstance(other, unicode):
128 other = other.encode("ascii")
129 try:
130 rounds, salt, digest = other.split("$")
131 except ValueError:
132 raise PasswordValueError, "invalid PBKDF2 hash (wrong number of separators)"
133 if rounds.startswith("0"):
134 raise PasswordValueError, "invalid PBKDF2 hash (zero-padded rounds)"
135 try:
136 rounds = int(rounds)
137 except ValueError:
138 raise PasswordValueError, "invalid PBKDF2 hash (invalid rounds)"
139 raw_salt = h64decode(salt)
140 else:
141 raw_salt = getrandbytes(20)
142 salt = h64encode(raw_salt)
143 #FIXME: find way to access config, so default rounds
144 # can be altered for faster/slower hosts via config.ini
145 rounds = 10000
146 if rounds < 1000:
147 raise PasswordValueError, "invalid PBKDF2 hash (rounds too low)"
148 raw_digest = pbkdf2(plaintext, raw_salt, rounds, 20)
149 return "%d$%s$%s" % (rounds, salt, h64encode(raw_digest))
150 elif scheme == 'SHA':
151 s = sha1(plaintext).hexdigest()
152 elif scheme == 'MD5':
153 s = md5(plaintext).hexdigest()
154 elif scheme == 'crypt' and crypt is not None:
155 if other is not None:
156 salt = other
157 else:
158 saltchars = './0123456789'+string.letters
159 salt = random.choice(saltchars) + random.choice(saltchars)
160 s = crypt.crypt(plaintext, salt)
161 elif scheme == 'plaintext':
162 s = plaintext
163 else:
164 raise PasswordValueError, 'unknown encryption scheme %r'%scheme
165 return s
167 def generatePassword(length=8):
168 chars = string.letters+string.digits
169 return ''.join([random.choice(chars) for x in range(length)])
171 class JournalPassword:
172 """ Password dummy instance intended for journal operation.
173 We do not store passwords in the journal any longer. The dummy
174 version only reads the encryption scheme from the given
175 encrypted password.
176 """
177 default_scheme = 'PBKDF2' # new encryptions use this scheme
178 pwre = re.compile(r'{(\w+)}(.+)')
180 def __init__ (self, encrypted=''):
181 if isinstance(encrypted, self.__class__):
182 self.scheme = encrypted.scheme or self.default_scheme
183 else:
184 m = self.pwre.match(encrypted)
185 if m:
186 self.scheme = m.group(1)
187 else:
188 self.scheme = self.default_scheme
189 self.password = ''
191 def dummystr(self):
192 """ return dummy string to store in journal
193 - reports scheme, but nothing else
194 """
195 return "{%s}*encrypted*" % (self.scheme,)
197 __str__ = dummystr
199 def __cmp__(self, other):
200 """Compare this password against another password."""
201 # check to see if we're comparing instances
202 if isinstance(other, self.__class__):
203 if self.scheme != other.scheme:
204 return cmp(self.scheme, other.scheme)
205 return cmp(self.password, other.password)
207 # assume password is plaintext
208 if self.password is None:
209 raise ValueError, 'Password not set'
210 return cmp(self.password, encodePassword(other, self.scheme,
211 self.password or None))
213 class Password(JournalPassword):
214 """The class encapsulates a Password property type value in the database.
216 The encoding of the password is one if None, 'SHA', 'MD5' or 'plaintext'.
217 The encodePassword function is used to actually encode the password from
218 plaintext. The None encoding is used in legacy databases where no
219 encoding scheme is identified.
221 The scheme is stored with the encoded data in the database:
222 {scheme}data
224 Example usage:
225 >>> p = Password('sekrit')
226 >>> p == 'sekrit'
227 1
228 >>> p != 'not sekrit'
229 1
230 >>> 'sekrit' == p
231 1
232 >>> 'not sekrit' != p
233 1
234 """
235 #TODO: code to migrate from old password schemes.
237 known_schemes = [ "PBKDF2", "SHA", "MD5", "crypt", "plaintext" ]
239 def __init__(self, plaintext=None, scheme=None, encrypted=None, strict=False):
240 """Call setPassword if plaintext is not None."""
241 if scheme is None:
242 scheme = self.default_scheme
243 if plaintext is not None:
244 self.setPassword (plaintext, scheme)
245 elif encrypted is not None:
246 self.unpack(encrypted, scheme, strict=strict)
247 else:
248 self.scheme = self.default_scheme
249 self.password = None
250 self.plaintext = None
252 def unpack(self, encrypted, scheme=None, strict=False):
253 """Set the password info from the scheme:<encryted info> string
254 (the inverse of __str__)
255 """
256 m = self.pwre.match(encrypted)
257 if m:
258 self.scheme = m.group(1)
259 self.password = m.group(2)
260 self.plaintext = None
261 else:
262 # currently plaintext - encrypt
263 self.setPassword(encrypted, scheme)
264 if strict and self.scheme not in self.known_schemes:
265 raise PasswordValueError, "unknown encryption scheme: %r" % (self.scheme,)
267 def setPassword(self, plaintext, scheme=None):
268 """Sets encrypts plaintext."""
269 if scheme is None:
270 scheme = self.default_scheme
271 self.scheme = scheme
272 self.password = encodePassword(plaintext, scheme)
273 self.plaintext = plaintext
275 def __str__(self):
276 """Stringify the encrypted password for database storage."""
277 if self.password is None:
278 raise ValueError, 'Password not set'
279 return '{%s}%s'%(self.scheme, self.password)
281 def test():
282 # SHA
283 p = Password('sekrit')
284 assert p == 'sekrit'
285 assert p != 'not sekrit'
286 assert 'sekrit' == p
287 assert 'not sekrit' != p
289 # MD5
290 p = Password('sekrit', 'MD5')
291 assert p == 'sekrit'
292 assert p != 'not sekrit'
293 assert 'sekrit' == p
294 assert 'not sekrit' != p
296 # crypt
297 p = Password('sekrit', 'crypt')
298 assert p == 'sekrit'
299 assert p != 'not sekrit'
300 assert 'sekrit' == p
301 assert 'not sekrit' != p
303 # PBKDF2 - low level function
304 from binascii import unhexlify
305 k = pbkdf2("password", "ATHENA.MIT.EDUraeburn", 1200, 32)
306 assert k == unhexlify("5c08eb61fdf71e4e4ec3cf6ba1f5512ba7e52ddbc5e5142f708a31e2e62b1e13")
308 # PBKDF2 - hash function
309 h = "5000$7BvbBq.EZzz/O0HuwX3iP.nAG3s$g3oPnFFaga2BJaX5PoPRljl4XIE"
310 assert encodePassword("sekrit", "PBKDF2", h) == h
312 # PBKDF2 - high level integration
313 p = Password('sekrit', 'PBKDF2')
314 assert p == 'sekrit'
315 assert p != 'not sekrit'
316 assert 'sekrit' == p
317 assert 'not sekrit' != p
319 if __name__ == '__main__':
320 test()
322 # vim: set filetype=python sts=4 sw=4 et si :