From 7b1b4996922e7ea4472bf37bd82c1ef6e2b534a3 Mon Sep 17 00:00:00 2001 From: Ian Good Date: Sun, 2 Oct 2022 12:02:13 -0400 Subject: [PATCH] Fix BuiltinHash hash name for pbkdf2-sha1 --- pysasl/hashing.py | 39 ++++++++++++++++++++++++++++----------- setup.py | 2 +- test/test_creds.py | 13 ++++++++++++- 3 files changed, 41 insertions(+), 13 deletions(-) diff --git a/pysasl/hashing.py b/pysasl/hashing.py index d0f4ada..397216a 100644 --- a/pysasl/hashing.py +++ b/pysasl/hashing.py @@ -9,7 +9,7 @@ from abc import abstractmethod from base64 import b64encode, b64decode from typing import TypeVar, Any, Optional, Sequence, Dict -from typing_extensions import Protocol, Final +from typing_extensions import Literal, Protocol, Final, TypeAlias try: from passlib.context import CryptContext @@ -19,6 +19,8 @@ __all__ = ['HashT', 'HashInterface', 'BuiltinHash', 'Cleartext', 'get_hash'] +_Pbkdf2Hashes: TypeAlias = Literal['sha1', 'sha256', 'sha512'] + #: Type variable for a :class:`HashInterface`. HashT = TypeVar('HashT', bound='HashInterface') @@ -83,20 +85,38 @@ class BuiltinHash(HashInterface): """ - __slots__: Sequence[str] = ['hash_name', 'salt_len', 'rounds'] + __slots__: Sequence[str] = ['hash_name', 'salt_len', 'rounds', + '_pbkdf2_hash'] - def __init__(self, *, hash_name: str = 'sha256', salt_len: int = 16, - rounds: int = 500000) -> None: + def __init__(self, *, hash_name: _Pbkdf2Hashes = 'sha256', + salt_len: int = 16, rounds: int = 500000) -> None: super().__init__() self.hash_name: Final = hash_name self.salt_len: Final = salt_len self.rounds: Final = rounds + self._pbkdf2_hash = self._to_pbkdf2_hash(hash_name) def _set_unless_none(self, kwargs: Dict[str, Any], key: str, val: Any) -> None: if val is not None: kwargs[key] = val + @classmethod + def _to_pbkdf2_hash(cls, hash_name: _Pbkdf2Hashes) -> str: + if hash_name == 'sha1': + return 'pbkdf2' + else: + return 'pbkdf2-' + hash_name + + @classmethod + def _from_pbkdf2_hash(cls, pbkdf2_hash: str) -> str: + if pbkdf2_hash == 'pbkdf2': + return 'sha1' + elif pbkdf2_hash.startswith('pbkdf2-'): + _, hash_name = pbkdf2_hash.split('-', 1) + return hash_name + raise ValueError(f'Invalid hash name: {pbkdf2_hash}') + def copy(self, *, hash_name: Optional[str] = None, salt_len: Optional[int] = None, rounds: Optional[int] = None, @@ -140,21 +160,18 @@ def hash(self, secret: str, salt: Optional[bytes] = None) -> str: """ if salt is None: # pragma: no cover salt = os.urandom(self.salt_len) - hash_name = self.hash_name rounds = self.rounds - digest = self._hash(hash_name, rounds, secret, salt) + digest = self._hash(self.hash_name, rounds, secret, salt) b64_salt = b64encode(salt).decode('ascii') b64_digest = b64encode(digest).decode('ascii') - return f'$pbkdf2-{hash_name}${rounds}${b64_salt}${b64_digest}' + return f'${self._pbkdf2_hash}${rounds}${b64_salt}${b64_digest}' def verify(self, secret: str, hash: str) -> bool: - prefix, hash_name, rounds_str, b64_salt, b64_digest = \ + prefix, pbkdf2_hash, rounds_str, b64_salt, b64_digest = \ hash.split('$', 4) if prefix != '': raise ValueError('Invalid hash prefix') - elif not hash_name.startswith('pbkdf2-'): - raise ValueError(f'Unrecognized hash name: {hash_name}') - _, hash_name = hash_name.split('-', 1) + hash_name = self._from_pbkdf2_hash(pbkdf2_hash) rounds = int(rounds_str) salt = b64decode(b64_salt) digest = b64decode(b64_digest) diff --git a/setup.py b/setup.py index 2eb273d..9492183 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ license = fh.read() setup(name='pysasl', - version='1.0.0.rc5', + version='1.0.0.rc6', author='Ian Good', author_email='ian@icgood.net', description='Pure Python SASL client and server library.', diff --git a/test/test_creds.py b/test/test_creds.py index c7d4992..0489e3f 100644 --- a/test/test_creds.py +++ b/test/test_creds.py @@ -11,6 +11,7 @@ builtin_hash = BuiltinHash(rounds=1000) b64_salt = 'bzstsT0hfnnXDUPTJqbkhQ==' +password_sha1 = '$pbkdf2$1000$' + b64_salt + '$ZreCYDHwQD8P81LbstmBx15gBgo=' password_sha256 = '$pbkdf2-sha256$1000$' + b64_salt + '$dWvL4bTpWfPobA2eti+k' \ 'CjUsF4sfwwiW58SE10p4Vh0=' password_sha512 = '$pbkdf2-sha512$1000$' + b64_salt + '$CTOIJXzOcorIOzxrRZVK' \ @@ -40,14 +41,24 @@ def test_hashed_builtin_copy(self) -> None: builtin_copy = builtin_hash.copy() stored = HashedIdentity('username', password_sha256, hash=builtin_copy) self.assertTrue(creds.verify(stored)) + builtin_copy = builtin_hash.copy(hash_name='sha1') + stored = HashedIdentity('username', password_sha1, hash=builtin_copy) + self.assertTrue(creds.verify(stored)) builtin_copy = builtin_hash.copy(hash_name='sha512') stored = HashedIdentity('username', password_sha512, hash=builtin_copy) self.assertTrue(creds.verify(stored)) def test_builtin_hash(self) -> None: salt = base64.b64decode(b64_salt) + builtin_copy = builtin_hash.copy() self.assertEqual(password_sha256, - builtin_hash.hash('password', salt)) + builtin_copy.hash('password', salt)) + builtin_copy = builtin_hash.copy(hash_name='sha1') + self.assertEqual(password_sha1, + builtin_copy.hash('password', salt)) + builtin_copy = builtin_hash.copy(hash_name='sha512') + self.assertEqual(password_sha512, + builtin_copy.hash('password', salt)) def test_builtin_hash_invalid(self) -> None: with self.assertRaises(ValueError):