commit/galaxy-central: james_taylor: Password security: use PBKDF2 scheme with sha256 for any new/changed passwords. Scheme is encoded in the database field to allow other schemes to be used in the future. If no recognized prefix is seen, sha1 is assumed.
1 new commit in galaxy-central: https://bitbucket.org/galaxy/galaxy-central/commits/a6720cff69b1/ Changeset: a6720cff69b1 User: james_taylor Date: 2013-05-05 18:17:33 Summary: Password security: use PBKDF2 scheme with sha256 for any new/changed passwords. Scheme is encoded in the database field to allow other schemes to be used in the future. If no recognized prefix is seen, sha1 is assumed. Affected #: 4 files diff -r 02822f28dc93348264a18f9f30abfedb53b80728 -r a6720cff69b1a4f142c9010c5d19f07c32b4475a lib/galaxy/model/__init__.py --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -21,6 +21,7 @@ import galaxy.datatypes import galaxy.datatypes.registry +import galaxy.security.passwords from galaxy.datatypes.metadata import MetadataCollection from galaxy.model.item_attrs import APIItem, UsesAnnotations from galaxy.security import get_permitted_actions @@ -86,13 +87,13 @@ """ Set user password to the digest of `cleartext`. """ - self.password = new_secure_hash( text_type=cleartext ) + self.password = galaxy.security.passwords.hash_password( cleartext ) def check_password( self, cleartext ): """ Check if `cleartext` matches user password when hashed. """ - return self.password == new_secure_hash( text_type=cleartext ) + return galaxy.security.passwords.check_password( cleartext, self.password ) def all_roles( self ): """ diff -r 02822f28dc93348264a18f9f30abfedb53b80728 -r a6720cff69b1a4f142c9010c5d19f07c32b4475a lib/galaxy/model/mapping.py --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -51,7 +51,7 @@ Column( "update_time", DateTime, default=now, onupdate=now ), Column( "email", TrimmedString( 255 ), index=True, nullable=False ), Column( "username", TrimmedString( 255 ), index=True, unique=True ), - Column( "password", TrimmedString( 40 ), nullable=False ), + Column( "password", TrimmedString( 255 ), nullable=False ), Column( "external", Boolean, default=False ), Column( "form_values_id", Integer, ForeignKey( "form_values.id" ), index=True ), Column( "deleted", Boolean, index=True, default=False ), diff -r 02822f28dc93348264a18f9f30abfedb53b80728 -r a6720cff69b1a4f142c9010c5d19f07c32b4475a lib/galaxy/model/migrate/versions/0115_longer_user_password_field.py --- /dev/null +++ b/lib/galaxy/model/migrate/versions/0115_longer_user_password_field.py @@ -0,0 +1,25 @@ +""" +Expand the length of the password fields in the galaxy_user table to allow for other hasing schemes +""" + +from sqlalchemy import * +from migrate import * + +import logging +log = logging.getLogger( __name__ ) + +def upgrade( migrate_engine ): + meta = MetaData( bind=migrate_engine ) + user = Table( 'galaxy_user', meta, autoload=True ) + try: + user.c.password.alter(type=String(255)) + except: + log.exception( "Altering password column failed" ) + +def downgrade(migrate_engine): + meta = MetaData( bind=migrate_engine ) + user = Table( 'galaxy_user', meta, autoload=True ) + try: + user.c.password.alter(type=String(40)) + except: + log.exception( "Altering password column failed" ) \ No newline at end of file diff -r 02822f28dc93348264a18f9f30abfedb53b80728 -r a6720cff69b1a4f142c9010c5d19f07c32b4475a lib/galaxy/security/passwords.py --- /dev/null +++ b/lib/galaxy/security/passwords.py @@ -0,0 +1,83 @@ +import hmac +import hashlib +from struct import Struct +from operator import xor +from itertools import izip, starmap +from os import urandom +from base64 import b64encode + +SALT_LENGTH = 12 +KEY_LENGTH = 24 +HASH_FUNCTION = 'sha256' +COST_FACTOR = 10000 + +def hash_password( password ): + """ + Hash a password, currently will use the PBKDF2 scheme. + """ + return hash_password_PBKDF2( password ) + +def check_password( guess, hashed ): + """ + Check a hashed password. Supports either PBKDF2 if the hash is + prefixed with that string, or sha1 otherwise. + """ + if hashed.startswith( "PBKDF2" ): + if check_password_PBKDF2( guess, hashed ): + return True + else: + # Passwords were originally encoded with sha1 and hexed + if hashlib.sha1( guess ).hexdigest() == hashed: + return True + # Password does not match + return False + +def hash_password_PBKDF2( password ): + # Generate a random salt + salt = b64encode( urandom( SALT_LENGTH ) ) + # Apply the pbkdf2 encoding + hashed = pbkdf2_bin( bytes( password ), salt, COST_FACTOR, KEY_LENGTH, getattr( hashlib, HASH_FUNCTION ) ) + # Format + return 'PBKDF2${0}${1}${2}${3}'.format( HASH_FUNCTION, COST_FACTOR, salt, b64encode( hashed ) ) + +def check_password_PBKDF2( guess, hashed ): + # Split the database representation to extract cost_factor and salt + name, hash_function, cost_factor, salt, encoded_original = hashed.split( '$', 5 ) + # Hash the guess using the same parameters + hashed_guess = pbkdf2_bin( bytes( guess ), salt, int( cost_factor ), KEY_LENGTH, getattr( hashlib, hash_function ) ) + encoded_guess = b64encode( hashed_guess ) + return safe_str_cmp( encoded_original, encoded_guess ) + +## Taken from https://github.com/mitsuhiko/python-pbkdf2/blob/master/pbkdf2.py +## (c) Copyright 2011 by Armin Ronacher, BSD LICENSE + +_pack_int = Struct('>I').pack + +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 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 \ No newline at end of file Repository URL: https://bitbucket.org/galaxy/galaxy-central/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email.
participants (1)
-
commits-noreply@bitbucket.org