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 portalocker
|
||||||
import logging
|
import logging
|
||||||
import marshal
|
import marshal
|
||||||
import numbers
|
|
||||||
import copy_reg
|
import copy_reg
|
||||||
from fileutils import abspath, listdir
|
from fileutils import abspath, listdir
|
||||||
import settings
|
import settings
|
||||||
@@ -34,6 +33,8 @@ markmin = lambda s: render( regex_param.sub(
|
|||||||
lambda m: '{' + markmin_escape(m.group('s')) + '}',
|
lambda m: '{' + markmin_escape(m.group('s')) + '}',
|
||||||
s ), sep='br', auto=False )
|
s ), sep='br', auto=False )
|
||||||
|
|
||||||
|
NUMBERS = (int,long,float)
|
||||||
|
|
||||||
# pattern to find T(blah blah blah) expressions
|
# pattern to find T(blah blah blah) expressions
|
||||||
PY_STRING_LITERAL_RE = r'(?<=[^\w]T\()(?P<name>'\
|
PY_STRING_LITERAL_RE = r'(?<=[^\w]T\()(?P<name>'\
|
||||||
+ r"[uU]?[rR]?(?:'''(?:[^']|'{1,2}(?!'))*''')|"\
|
+ r"[uU]?[rR]?(?:'''(?:[^']|'{1,2}(?!'))*''')|"\
|
||||||
@@ -145,7 +146,7 @@ def read_dict_aux(filename):
|
|||||||
return {}
|
return {}
|
||||||
try:
|
try:
|
||||||
return eval(lang_text)
|
return eval(lang_text)
|
||||||
except Exception as e:
|
except Exception, e:
|
||||||
status='Syntax error in %s (%s)' % (filename, e)
|
status='Syntax error in %s (%s)' % (filename, e)
|
||||||
logging.error(status)
|
logging.error(status)
|
||||||
return {'__corrupted__':status}
|
return {'__corrupted__':status}
|
||||||
@@ -238,7 +239,7 @@ def read_plural_rules_aux(filename):
|
|||||||
construct_plural_form=locals().get('construct_plural_form',
|
construct_plural_form=locals().get('construct_plural_form',
|
||||||
default_construct_plural_form)
|
default_construct_plural_form)
|
||||||
status='ok'
|
status='ok'
|
||||||
except Exception as e:
|
except Exception, e:
|
||||||
nplurals=default_nplurals
|
nplurals=default_nplurals
|
||||||
get_plural_id=default_get_plural_id
|
get_plural_id=default_get_plural_id
|
||||||
construct_plural_form=default_construct_plural_form
|
construct_plural_form=default_construct_plural_form
|
||||||
@@ -325,7 +326,7 @@ def read_plural_dict_aux(filename):
|
|||||||
return {}
|
return {}
|
||||||
try:
|
try:
|
||||||
return eval(lang_text)
|
return eval(lang_text)
|
||||||
except Exception as e:
|
except Exception, e:
|
||||||
status='Syntax error in %s (%s)' % (filename, e)
|
status='Syntax error in %s (%s)' % (filename, e)
|
||||||
logging.error(status)
|
logging.error(status)
|
||||||
return {'__corrupted__':status}
|
return {'__corrupted__':status}
|
||||||
@@ -701,11 +702,11 @@ class translator(object):
|
|||||||
if isinstance(symbols, dict):
|
if isinstance(symbols, dict):
|
||||||
symbols.update( (key, xmlescape(value).translate(ttab_in))
|
symbols.update( (key, xmlescape(value).translate(ttab_in))
|
||||||
for key, value in symbols.iteritems()
|
for key, value in symbols.iteritems()
|
||||||
if not isinstance(value, numbers.Number) )
|
if not isinstance(value, NUMBERS) )
|
||||||
else:
|
else:
|
||||||
if not isinstance(symbols, tuple):
|
if not isinstance(symbols, tuple):
|
||||||
symbols = (symbols,)
|
symbols = (symbols,)
|
||||||
symbols = tuple(value if isinstance(value, numbers.Number)
|
symbols = tuple(value if isinstance(value, NUMBERS)
|
||||||
else xmlescape(value).translate(ttab_in)
|
else xmlescape(value).translate(ttab_in)
|
||||||
for value in symbols)
|
for value in symbols)
|
||||||
message = self.params_substitution(message, symbols)
|
message = self.params_substitution(message, symbols)
|
||||||
@@ -857,11 +858,11 @@ class translator(object):
|
|||||||
if isinstance(symbols, dict):
|
if isinstance(symbols, dict):
|
||||||
symbols.update( (key, str(value).translate(ttab_in))
|
symbols.update( (key, str(value).translate(ttab_in))
|
||||||
for key, value in symbols.iteritems()
|
for key, value in symbols.iteritems()
|
||||||
if not isinstance(value, numbers.Number) )
|
if not isinstance(value, NUMBERS) )
|
||||||
else:
|
else:
|
||||||
if not isinstance(symbols, tuple):
|
if not isinstance(symbols, tuple):
|
||||||
symbols = (symbols,)
|
symbols = (symbols,)
|
||||||
symbols = tuple(value if isinstance(value, numbers.Number)
|
symbols = tuple(value if isinstance(value, NUMBERS)
|
||||||
else str(value).translate(ttab_in)
|
else str(value).translate(ttab_in)
|
||||||
for value in symbols)
|
for value in symbols)
|
||||||
|
|
||||||
|
|||||||
+9
-7
@@ -16,6 +16,7 @@ import random
|
|||||||
import time
|
import time
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
from gluon.contrib.pbkdf2 import pbkdf2_hex
|
||||||
|
|
||||||
logger = logging.getLogger("web2py")
|
logger = logging.getLogger("web2py")
|
||||||
|
|
||||||
@@ -32,7 +33,7 @@ def md5_hash(text):
|
|||||||
""" Generate a md5 hash with the given text """
|
""" Generate a md5 hash with the given text """
|
||||||
return hashlib.md5(text).hexdigest()
|
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
|
Generates hash with the given text using the specified
|
||||||
digest hashing algorithm
|
digest hashing algorithm
|
||||||
@@ -41,6 +42,8 @@ def simple_hash(text, digest_alg = 'md5'):
|
|||||||
raise RuntimeError, "simple_hash with digest_alg=None"
|
raise RuntimeError, "simple_hash with digest_alg=None"
|
||||||
elif not isinstance(digest_alg,str):
|
elif not isinstance(digest_alg,str):
|
||||||
h = digest_alg(text)
|
h = digest_alg(text)
|
||||||
|
elif salt:
|
||||||
|
return hmac_hash(text, salt, digest_alg)
|
||||||
else:
|
else:
|
||||||
h = hashlib.new(digest_alg)
|
h = hashlib.new(digest_alg)
|
||||||
h.update(text)
|
h.update(text)
|
||||||
@@ -68,13 +71,12 @@ def get_digest(value):
|
|||||||
else:
|
else:
|
||||||
raise ValueError("Invalid digest algorithm")
|
raise ValueError("Invalid digest algorithm")
|
||||||
|
|
||||||
def hmac_hash(value, key, digest_alg='md5', salt=None):
|
def hmac_hash(value, salt, digest_alg='md5'):
|
||||||
if ':' in key:
|
if isinstance(digest_alg,str) and digest_alg.startswith('pbkdf2'):
|
||||||
digest_alg, key = key.split(':')
|
iterations, keylen = digest_alg[7:-1].split(',')
|
||||||
|
return pbkdf2_hex(value, salt, int(iterations), int(keylen))
|
||||||
digest_alg = get_digest(digest_alg)
|
digest_alg = get_digest(digest_alg)
|
||||||
d = hmac.new(key,value,digest_alg)
|
d = hmac.new(salt,value,digest_alg)
|
||||||
if salt:
|
|
||||||
d.update(str(salt))
|
|
||||||
return d.hexdigest()
|
return d.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+34
-24
@@ -2535,45 +2535,55 @@ class LazyCrypt(object):
|
|||||||
"""
|
"""
|
||||||
Encrypted self.password and caches it in self.crypted.
|
Encrypted self.password and caches it in self.crypted.
|
||||||
If self.crypt.salt the output is in the format <algorithm>$<salt>$<hash>
|
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:
|
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 self.crypt.salt:
|
||||||
if not self.crypt.key:
|
|
||||||
raise RuntimeError, "CRYPT has salt but not key"
|
|
||||||
if self.crypt.salt == True:
|
if self.crypt.salt == True:
|
||||||
salt = str(web2py_uuid()).replace('-','')[-16:]
|
salt = str(web2py_uuid()).replace('-','')[-16:]
|
||||||
else:
|
else:
|
||||||
salt = self.crypt.salt
|
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:
|
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
|
return self.crypted
|
||||||
|
|
||||||
def __eq__(self, stored_password):
|
def __eq__(self, stored_password):
|
||||||
"""
|
"""
|
||||||
compares the current lazy crypted password with a stored password
|
compares the current lazy crypted password with a stored password
|
||||||
"""
|
"""
|
||||||
if self.crypt.salt and stored_password.count('$')==2:
|
if self.crypt.salt and stored_password.count('$')==2:
|
||||||
if ':' in self.crypt.key:
|
key = self.crypt.key.split(':')[1] if ':' in self.crypt.key else ''
|
||||||
hash_key = self.crypt.key.split(':')[1]
|
(digest_alg, salt, hash) = stored_password.split('$')
|
||||||
else:
|
masterkey = key+salt
|
||||||
hash_key = None
|
h = simple_hash(self.password, masterkey, digest_alg)
|
||||||
(algorithm, salt, hash) = stored_password.split('$')
|
temp_pass = '%s$%s$%s' % (digest_alg, salt, h)
|
||||||
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)
|
|
||||||
else:
|
else:
|
||||||
temp_pass = str(self)
|
temp_pass = str(self)
|
||||||
return temp_pass == stored_password
|
return temp_pass == stored_password
|
||||||
|
|||||||
Reference in New Issue
Block a user