added pbkdf2 support, simplified logic
This commit is contained in:
@@ -1 +1 @@
|
||||
Version 2.00.0 (2012-07-18 11:22:36) dev
|
||||
Version 2.00.0 (2012-07-18 12:24:23) dev
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user