added pbkdf2 support, simplified logic

This commit is contained in:
mdipierro
2012-07-18 12:24:28 -05:00
parent 9cfb1e8afd
commit 1e657f0121
5 changed files with 185 additions and 42 deletions
+1 -1
View File
@@ -1 +1 @@
Version 2.00.0 (2012-07-18 11:22:36) dev
Version 2.00.0 (2012-07-18 12:24:23) dev
+130
View File
@@ -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()
+9 -8
View File
@@ -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<name>'\
+ 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)
+9 -7
View File
@@ -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()
+36 -26
View File
@@ -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 <algorithm>$<salt>$<hash>
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