diff --git a/sqlalchemy_utils/types/password.py b/sqlalchemy_utils/types/password.py index ade3524..ec6d1c6 100644 --- a/sqlalchemy_utils/types/password.py +++ b/sqlalchemy_utils/types/password.py @@ -48,14 +48,15 @@ class PasswordType(types.TypeDecorator, ScalarCoercible): """ - impl = types.BINARY(60) + impl = types.VARBINARY(1024) python_type = Password - def __init__(self, **kwargs): + def __init__(self, max_length=None, **kwargs): """ - All keyword arguments are forwarded to the construction of a - `passlib.context.CryptContext` object. + All keyword arguments (aside from max_length) are + forwarded to the construction of a `passlib.context.CryptContext` + object. The following usage will create a password column that will automatically hash new passwords as `pbkdf2_sha512` but still compare @@ -84,6 +85,25 @@ class PasswordType(types.TypeDecorator, ScalarCoercible): # Construct the passlib crypt context. self.context = CryptContext(**kwargs) + if max_length is None: + # Calculate the largest possible encoded password. + # name + rounds + salt + hash + ($ * 4) of largest hash + max_lengths = [] + for name in self.context.schemes(): + scheme = getattr(__import__('passlib.hash').hash, name) + length = 4 + len(scheme.name) + length += len(str(getattr(scheme, 'max_rounds', ''))) + length += scheme.max_salt_size or 0 + length += getattr(scheme, 'encoded_checksum_size', + scheme.checksum_size) + max_lengths.append(length) + + # Set the max_length to the maximum calculated max length. + max_length = max(max_lengths) + + # Set the impl to the now-calculated max length. + self.impl = types.VARBINARY(max_length) + def process_bind_param(self, value, dialect): if isinstance(value, Password): # Value has already been hashed. diff --git a/tests/types/test_password.py b/tests/types/test_password.py index f212fbd..da4209c 100644 --- a/tests/types/test_password.py +++ b/tests/types/test_password.py @@ -1,6 +1,7 @@ from pytest import mark import sqlalchemy as sa from tests import TestCase +from sqlalchemy import inspect from sqlalchemy_utils.types import password from sqlalchemy_utils import Password, PasswordType @@ -15,6 +16,7 @@ class TestPasswordType(TestCase): password = sa.Column(PasswordType( schemes=[ 'pbkdf2_sha512', + 'pbkdf2_sha256', 'md5_crypt' ], @@ -67,3 +69,20 @@ class TestPasswordType(TestCase): assert obj.password.hash.startswith('$1$') assert obj.password == 'b' assert obj.password.hash.startswith('$pbkdf2-sha512$') + + def test_auto_column_length(self): + """Should derive the correct column length from the specified schemes. + """ + + from passlib.hash import pbkdf2_sha512 + + impl = inspect(self.User).c.password.type.impl + + # name + rounds + salt + hash + ($ * 4) of largest hash + expected_length = len(pbkdf2_sha512.name) + expected_length += len(str(pbkdf2_sha512.max_rounds)) + expected_length += pbkdf2_sha512.max_salt_size + expected_length += pbkdf2_sha512.encoded_checksum_size + expected_length += 4 + + assert impl.length == expected_length