diff --git a/VERSION b/VERSION index 318550d5..e1d7cada 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.00.0 (2012-07-18 11:22:36) dev +Version 2.00.0 (2012-07-18 12:24:23) dev diff --git a/gluon/contrib/pbkdf2.py b/gluon/contrib/pbkdf2.py new file mode 100644 index 00000000..b7a7dd42 --- /dev/null +++ b/gluon/contrib/pbkdf2.py @@ -0,0 +1,130 @@ +# -*- coding: utf-8 -*- +""" + pbkdf2 + ~~~~~~ + + This module implements pbkdf2 for Python. It also has some basic + tests that ensure that it works. The implementation is straightforward + and uses stdlib only stuff and can be easily be copy/pasted into + your favourite application. + + Use this as replacement for bcrypt that does not need a c implementation + of a modified blowfish crypto algo. + + Example usage: + + >>> pbkdf2_hex('what i want to hash', 'the random salt') + 'fa7cc8a2b0a932f8e6ea42f9787e9d36e592e0c222ada6a9' + + How to use this: + + 1. Use a constant time string compare function to compare the stored hash + with the one you're generating:: + + def safe_str_cmp(a, b): + if len(a) != len(b): + return False + rv = 0 + for x, y in izip(a, b): + rv |= ord(x) ^ ord(y) + return rv == 0 + + 2. Use `os.urandom` to generate a proper salt of at least 8 byte. + Use a unique salt per hashed password. + + 3. Store ``algorithm$salt:costfactor$hash`` in the database so that + you can upgrade later easily to a different algorithm if you need + one. For instance ``PBKDF2-256$thesalt:10000$deadbeef...``. + + + :copyright: (c) Copyright 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import hmac +import hashlib +from struct import Struct +from operator import xor +from itertools import izip, starmap + + +_pack_int = Struct('>I').pack + + +def pbkdf2_hex(data, salt, iterations=1000, keylen=24, hashfunc=None): + """Like :func:`pbkdf2_bin` but returns a hex encoded string.""" + return pbkdf2_bin(data, salt, iterations, keylen, hashfunc).encode('hex') + + +def pbkdf2_bin(data, salt, iterations=1000, keylen=24, hashfunc=None): + """Returns a binary digest for the PBKDF2 hash algorithm of `data` + with the given `salt`. It iterates `iterations` time and produces a + key of `keylen` bytes. By default SHA-1 is used as hash function, + a different hashlib `hashfunc` can be provided. + """ + hashfunc = hashfunc or hashlib.sha1 + mac = hmac.new(data, None, hashfunc) + def _pseudorandom(x, mac=mac): + h = mac.copy() + h.update(x) + return map(ord, h.digest()) + buf = [] + for block in xrange(1, -(-keylen // mac.digest_size) + 1): + rv = u = _pseudorandom(salt + _pack_int(block)) + for i in xrange(iterations - 1): + u = _pseudorandom(''.join(map(chr, u))) + rv = starmap(xor, izip(rv, u)) + buf.extend(rv) + return ''.join(map(chr, buf))[:keylen] + + +def test(): + failed = [] + def check(data, salt, iterations, keylen, expected): + rv = pbkdf2_hex(data, salt, iterations, keylen) + if rv != expected: + print 'Test failed:' + print ' Expected: %s' % expected + print ' Got: %s' % rv + print ' Parameters:' + print ' data=%s' % data + print ' salt=%s' % salt + print ' iterations=%d' % iterations + print + failed.append(1) + + # From RFC 6070 + check('password', 'salt', 1, 20, + '0c60c80f961f0e71f3a9b524af6012062fe037a6') + check('password', 'salt', 2, 20, + 'ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957') + check('password', 'salt', 4096, 20, + '4b007901b765489abead49d926f721d065a429c1') + check('passwordPASSWORDpassword', 'saltSALTsaltSALTsaltSALTsaltSALTsalt', + 4096, 25, '3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038') + check('pass\x00word', 'sa\x00lt', 4096, 16, + '56fa6aa75548099dcc37d7f03425e0c3') + # This one is from the RFC but it just takes for ages + ##check('password', 'salt', 16777216, 20, + ## 'eefe3d61cd4da4e4e9945b3d6ba2158c2634e984') + + # From Crypt-PBKDF2 + check('password', 'ATHENA.MIT.EDUraeburn', 1, 16, + 'cdedb5281bb2f801565a1122b2563515') + check('password', 'ATHENA.MIT.EDUraeburn', 1, 32, + 'cdedb5281bb2f801565a1122b25635150ad1f7a04bb9f3a333ecc0e2e1f70837') + check('password', 'ATHENA.MIT.EDUraeburn', 2, 16, + '01dbee7f4a9e243e988b62c73cda935d') + check('password', 'ATHENA.MIT.EDUraeburn', 2, 32, + '01dbee7f4a9e243e988b62c73cda935da05378b93244ec8f48a99e61ad799d86') + check('password', 'ATHENA.MIT.EDUraeburn', 1200, 32, + '5c08eb61fdf71e4e4ec3cf6ba1f5512ba7e52ddbc5e5142f708a31e2e62b1e13') + check('X' * 64, 'pass phrase equals block size', 1200, 32, + '139c30c0966bc32ba55fdbf212530ac9c5ec59f1a452f5cc9ad940fea0598ed1') + check('X' * 65, 'pass phrase exceeds block size', 1200, 32, + '9ccad6d468770cd51b10e6a68721be611a8b4d282601db3b36be9246915ec82a') + + raise SystemExit(bool(failed)) + + +if __name__ == '__main__': + test() diff --git a/gluon/languages.py b/gluon/languages.py index 1008e542..8123db67 100644 --- a/gluon/languages.py +++ b/gluon/languages.py @@ -17,7 +17,6 @@ from cgi import escape import portalocker import logging import marshal -import numbers import copy_reg from fileutils import abspath, listdir import settings @@ -34,6 +33,8 @@ markmin = lambda s: render( regex_param.sub( lambda m: '{' + markmin_escape(m.group('s')) + '}', s ), sep='br', auto=False ) +NUMBERS = (int,long,float) + # pattern to find T(blah blah blah) expressions PY_STRING_LITERAL_RE = r'(?<=[^\w]T\()(?P'\ + r"[uU]?[rR]?(?:'''(?:[^']|'{1,2}(?!'))*''')|"\ @@ -145,7 +146,7 @@ def read_dict_aux(filename): return {} try: return eval(lang_text) - except Exception as e: + except Exception, e: status='Syntax error in %s (%s)' % (filename, e) logging.error(status) return {'__corrupted__':status} @@ -238,7 +239,7 @@ def read_plural_rules_aux(filename): construct_plural_form=locals().get('construct_plural_form', default_construct_plural_form) status='ok' - except Exception as e: + except Exception, e: nplurals=default_nplurals get_plural_id=default_get_plural_id construct_plural_form=default_construct_plural_form @@ -325,7 +326,7 @@ def read_plural_dict_aux(filename): return {} try: return eval(lang_text) - except Exception as e: + except Exception, e: status='Syntax error in %s (%s)' % (filename, e) logging.error(status) return {'__corrupted__':status} @@ -701,11 +702,11 @@ class translator(object): if isinstance(symbols, dict): symbols.update( (key, xmlescape(value).translate(ttab_in)) for key, value in symbols.iteritems() - if not isinstance(value, numbers.Number) ) + if not isinstance(value, NUMBERS) ) else: if not isinstance(symbols, tuple): symbols = (symbols,) - symbols = tuple(value if isinstance(value, numbers.Number) + symbols = tuple(value if isinstance(value, NUMBERS) else xmlescape(value).translate(ttab_in) for value in symbols) message = self.params_substitution(message, symbols) @@ -857,11 +858,11 @@ class translator(object): if isinstance(symbols, dict): symbols.update( (key, str(value).translate(ttab_in)) for key, value in symbols.iteritems() - if not isinstance(value, numbers.Number) ) + if not isinstance(value, NUMBERS) ) else: if not isinstance(symbols, tuple): symbols = (symbols,) - symbols = tuple(value if isinstance(value, numbers.Number) + symbols = tuple(value if isinstance(value, NUMBERS) else str(value).translate(ttab_in) for value in symbols) diff --git a/gluon/utils.py b/gluon/utils.py index 99780351..ad690ed7 100644 --- a/gluon/utils.py +++ b/gluon/utils.py @@ -16,6 +16,7 @@ import random import time import os import logging +from gluon.contrib.pbkdf2 import pbkdf2_hex logger = logging.getLogger("web2py") @@ -32,7 +33,7 @@ def md5_hash(text): """ Generate a md5 hash with the given text """ return hashlib.md5(text).hexdigest() -def simple_hash(text, digest_alg = 'md5'): +def simple_hash(text, salt = '', digest_alg = 'md5'): """ Generates hash with the given text using the specified digest hashing algorithm @@ -41,6 +42,8 @@ def simple_hash(text, digest_alg = 'md5'): raise RuntimeError, "simple_hash with digest_alg=None" elif not isinstance(digest_alg,str): h = digest_alg(text) + elif salt: + return hmac_hash(text, salt, digest_alg) else: h = hashlib.new(digest_alg) h.update(text) @@ -68,13 +71,12 @@ def get_digest(value): else: raise ValueError("Invalid digest algorithm") -def hmac_hash(value, key, digest_alg='md5', salt=None): - if ':' in key: - digest_alg, key = key.split(':') +def hmac_hash(value, salt, digest_alg='md5'): + if isinstance(digest_alg,str) and digest_alg.startswith('pbkdf2'): + iterations, keylen = digest_alg[7:-1].split(',') + return pbkdf2_hex(value, salt, int(iterations), int(keylen)) digest_alg = get_digest(digest_alg) - d = hmac.new(key,value,digest_alg) - if salt: - d.update(str(salt)) + d = hmac.new(salt,value,digest_alg) return d.hexdigest() diff --git a/gluon/validators.py b/gluon/validators.py index ace9db24..3aceaccd 100644 --- a/gluon/validators.py +++ b/gluon/validators.py @@ -2535,45 +2535,55 @@ class LazyCrypt(object): """ Encrypted self.password and caches it in self.crypted. If self.crypt.salt the output is in the format $$ + + Try get the digest_alg from the key (if it exists) + else assume the default digest_alg. If not key at all, set key='' + + If a salt is specified use it, if salt is True, set salt to uuid + + masterkey is the key (as specified in argument) + salt + if masterkey is '' then simple_hash does not do HMAC + else simple_hash calls hmac_hash + (this should all be backward compatible) + + Options: + key = 'uuid' + key = 'md5:uuid' + key = 'sha512:uuid' + ... + key = 'pbkdf2(100,64):uuid' 100 iterations and 64 chars length """ if self.crypted: - return self.crypted + return self.crypted + if self.crypt.key: + if ':' in self.crypt.key: + digest_alg, key = self.crypt.key.split(':',1) + else: + digest_alg, key = self.crypt.digest_alg, self.crypt.key + else: + digest_alg, key = self.crypt.digest_alg, '' if self.crypt.salt: - if not self.crypt.key: - raise RuntimeError, "CRYPT has salt but not key" if self.crypt.salt == True: - salt = str(web2py_uuid()).replace('-','')[-16:] + salt = str(web2py_uuid()).replace('-','')[-16:] else: salt = self.crypt.salt - if ':' in self.crypt.key: - (alg, hash_key) = self.crypt.key.split(':') - else: - (alg, hash_key) = self.crypt.digest_alg, None - if hash_key: - h = hmac_hash(self.password+salt, self.crypt.key, alg) - else: - h = imple_hash(self.password+salt, alg) - self.crypted = '%s$%s$%s' % (alg, salt, h) - elif self.crypt.key: - self.crypted = hmac_hash(self.password, self.crypt.key, self.crypt.digest_alg) else: - self.crypted = simple_hash(self.password, self.crypt.digest_alg) + salt = '' + masterkey = key+salt + h = simple_hash(self.password, masterkey, digest_alg) + self.crypted = '%s$%s$%s' % (digest_alg, salt, h) return self.crypted + def __eq__(self, stored_password): """ compares the current lazy crypted password with a stored password """ if self.crypt.salt and stored_password.count('$')==2: - if ':' in self.crypt.key: - hash_key = self.crypt.key.split(':')[1] - else: - hash_key = None - (algorithm, salt, hash) = stored_password.split('$') - if hash_key: - h = hmac_hash(self.password+salt, self.crypt.key, algorithm) - else: - h = simple_hash(self.password+salt, algorithm) - temp_pass = '%s$%s$%s' % (algorithm, salt, h) + key = self.crypt.key.split(':')[1] if ':' in self.crypt.key else '' + (digest_alg, salt, hash) = stored_password.split('$') + masterkey = key+salt + h = simple_hash(self.password, masterkey, digest_alg) + temp_pass = '%s$%s$%s' % (digest_alg, salt, h) else: temp_pass = str(self) return temp_pass == stored_password