Skip to content

Commit

Permalink
Fix BuiltinHash hash name for pbkdf2-sha1
Browse files Browse the repository at this point in the history
  • Loading branch information
icgood committed Oct 2, 2022
1 parent 491a4b0 commit a43f5c9
Show file tree
Hide file tree
Showing 3 changed files with 43 additions and 13 deletions.
41 changes: 30 additions & 11 deletions pysasl/hashing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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')

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -140,21 +160,20 @@ 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)
print('wtf', pbkdf2_hash)
hash_name = self._from_pbkdf2_hash(pbkdf2_hash)
print('lol', hash_name)
rounds = int(rounds_str)
salt = b64decode(b64_salt)
digest = b64decode(b64_digest)
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand Down
13 changes: 12 additions & 1 deletion test/test_creds.py
Original file line number Diff line number Diff line change
Expand Up @@ -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' \
Expand Down Expand Up @@ -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):
Expand Down

0 comments on commit a43f5c9

Please sign in to comment.