Skip to content

Commit

Permalink
v2.4.0: finished refactoring
Browse files Browse the repository at this point in the history
Added src/hashpw/algs/PBKDF2.py (not yet used)
  • Loading branch information
unixnut committed Sep 6, 2024
1 parent 9a6f61e commit 73cce0c
Show file tree
Hide file tree
Showing 24 changed files with 719 additions and 603 deletions.
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 2.3.0
current_version = 2.4.0
commit = False
tag = False
allow_dirty = True
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
packages=find_packages('src'),
package_dir={'': 'src'},
url='https://github.com/unixnut/hashpw',
version="2.3.0",
version="2.4.0",
zip_safe=False,
)

Expand Down
67 changes: 66 additions & 1 deletion src/hashpw/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,69 @@

__author__ = """Alastair Irvine"""
__email__ = 'alastair@plug.org.au'
__version__ = '2.3.0'
__version__ = '2.4.0'


from typing import Set, Dict, Sequence, Tuple, List, Union, AnyStr, Iterable, Callable, Generator, Type, Optional, TextIO, IO

from .algs.ApacheMD5 import ApacheMD5
from .algs.ApacheSHA1 import ApacheSHA1
from .algs.BasicMD5 import BasicMD5
from .algs.Blowfish import Blowfish
from .algs.Crypt import Crypt
from .algs.ExtDes import ExtDes
from .algs.MD5 import MD5
from .algs.MySqlSHA1 import MySqlSHA1
from .algs.OldPassword import OldPassword
## from .algs.PBKDF2 import PBKDF2
from .algs.PhpBB3 import PhpBB3
from .algs.Phpass import Phpass
from .algs.SHA256 import SHA256
from .algs.SHA512 import SHA512
from .algs.SSHA import SSHA


# *** DEFINITIONS ***
# Algorithms with longer prefixes need to appear earlier in this list
algorithms = (MD5, ApacheMD5, ApacheSHA1, Blowfish, SHA256, SHA512, MySqlSHA1, Phpass, PhpBB3, SSHA, BasicMD5, ExtDes, Crypt, OldPassword)
## PBKDF2,


# *** FUNCTIONS ***
# Set by init()
recognise_algorithm = None


def recognise_algorithm_by_hash(algorithm, s):
return algorithm.recognise_full(s)


def recognise_algorithm_by_salt(algorithm, s):
if algorithm.supports_salt:
return algorithm.recognise_salt(s)
else:
return algorithm.recognise_full(s)
## return False


def identify_salt(salt: str) -> Optional[Tuple[str, Type]]:
for a in algorithms:
if recognise_algorithm(a, salt):
# mode, alg_class
return a.name, a


def init(settings: Dict):
global recognise_algorithm

if settings['verify']:
recognise_algorithm = recognise_algorithm_by_hash
else:
recognise_algorithm = recognise_algorithm_by_salt

# have to do this after option handling but before algorithm recognition
for a in algorithms:
a.init(a, long_salt=settings['long_salt'])


# *** MAINLINE ***
18 changes: 18 additions & 0 deletions src/hashpw/algs/ApacheMD5.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import passlib.hash

from ..structure import PLSaltedAlgorithm


class ApacheMD5(PLSaltedAlgorithm):
name = "apache-md5"
option = "a"
prefix = "$apr1$"
suffix = ""
min_length = 37
salt_length = 8


def __init__(self, salt):
super().__init__(salt)

self.hasher = passlib.hash.apr_md5_crypt.using(salt=self.salt[6:])
18 changes: 18 additions & 0 deletions src/hashpw/algs/ApacheSHA1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import hashlib

from .. import utils
from ..structure import Algorithm


class ApacheSHA1(Algorithm):
name = "apache-sha-1"
option = "A"
prefix = "{SHA}"
suffix = ""
min_length = 33


def hash(self, plaintext):
input_byte_str = plaintext.encode("UTF-8")
round_output = hashlib.sha1(input_byte_str).digest()
return self.prefix + utils.base64encode(round_output)
20 changes: 20 additions & 0 deletions src/hashpw/algs/BasicMD5.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import binascii
import hashlib

from ..structure import Algorithm


class BasicMD5(Algorithm):
name = "basic-md5"
option = "M"
prefix = ""
extra_prefix = "{PLAIN-MD5}"
suffix = ""
min_length = 32


def hash(self, plaintext):
input_byte_str = plaintext.encode("UTF-8")
first_round_output = hashlib.md5(input_byte_str).digest()
output_byte_str = binascii.hexlify(first_round_output)
return output_byte_str.decode('ascii')
38 changes: 38 additions & 0 deletions src/hashpw/algs/Blowfish.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from ..structure import SaltedAlgorithm


class Blowfish(SaltedAlgorithm):
"""See https://pypi.org/project/py-bcrypt/"""
name = "blowfish"
option = "b"
prefix = "$2a$"
extra_prefix = "{BLF-CRYPT}"
suffix = ""
min_length = 57
salt_length = 16


@classmethod
def final_prep(c):
"""[Override]"""
c.rounds=13

# Pass it up the hierarchy
SaltedAlgorithm.final_prep()

global bcrypt
import bcrypt


## def __init__(self, salt):
## super().__init__(salt)


def hash(self, plaintext):
return bcrypt.hashpw(plaintext, self.salt)


@classmethod
def generate_salt(c):
"""Calculates an encoded salt string, including prefix, for this algorithm."""
return bcrypt.gensalt(log_rounds=c.rounds)
15 changes: 15 additions & 0 deletions src/hashpw/algs/Crypt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from ..structure import SaltedAlgorithm


class Crypt(SaltedAlgorithm):
name = "crypt"
option = "c"
prefix = ""
suffix = ""
min_length = 13
salt_length = 2


@classmethod
def recognise_full(c, s):
return len(s) == c.min_length
15 changes: 15 additions & 0 deletions src/hashpw/algs/ExtDes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from ..structure import SaltedAlgorithm


class ExtDes(SaltedAlgorithm):
name = "ext-des"
option = "x"
prefix = "_"
suffix = ""
min_length = 20
salt_length = 8


## @classmethod
## def recognise_salt(c, s):
## return False
10 changes: 10 additions & 0 deletions src/hashpw/algs/MD5.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from ..structure import SaltedAlgorithm


class MD5(SaltedAlgorithm):
name = "md5"
option = "x"
prefix = "$1$"
suffix = ""
min_length = 34
salt_length = 8
20 changes: 20 additions & 0 deletions src/hashpw/algs/MySqlSHA1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import binascii
import hashlib

from ..structure import Algorithm


class MySqlSHA1(Algorithm):
name = "mysql-sha-1"
option = "p"
prefix = "*"
suffix = ""
min_length = 41


def hash(self, plaintext):
input_byte_str = plaintext.encode("UTF-8")
first_round_output = hashlib.sha1(input_byte_str).digest()
second_round_output = hashlib.sha1(first_round_output).digest()
output_byte_str = binascii.hexlify(second_round_output)
return "*" + output_byte_str.decode('ascii').upper()
30 changes: 30 additions & 0 deletions src/hashpw/algs/OldPassword.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from ..structure import Algorithm


class OldPassword(Algorithm):
"""Pre-v4.1 MySQL, and also newer with the 'old-passwords' setting on
http://djangosnippets.org/snippets/1508/"""
name = "old-password"
option = "o"
prefix = ""
suffix = ""
min_length = 16


@classmethod
def final_prep(c):
"""[Override]"""
# Pass it up the hierarchy
Algorithm.final_prep()

global mysql_hash_password
from hashpw.contrib.tback import mysql_hash_password


def recognise(self, s):
return False


def hash(self, plaintext):
return mysql_hash_password(plaintext)
55 changes: 55 additions & 0 deletions src/hashpw/algs/PBKDF2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import passlib.hash
import passlib.utils

from ..structure import PLSaltedAlgorithm


class PBKDF2(PLSaltedAlgorithm):
name = "django-pbkdf2"
option = "d"
prefix = "pbkdf2_sha256"
suffix = ""
min_length = 70 # prefix + '$' + rounds(2 chars) + '$' + 44 chars


# This can't be a @classmethod because parent classes have to work with its properties
@staticmethod
def init(c, *, long_salt):
"""Ensure that check_salt() checks the length of the whole hash."""

if long_salt:
c.salt_length = 16
else:
c.salt_length = 8
PLSaltedAlgorithm.init(c)

## c.rounds=260000
c.rounds=300000
c.salt_prefix_len = 21 # pbkdf2_sha256$260000$
c.comp_len = c.salt_prefix_len + c.salt_length


@classmethod
def generate_salt(c):
"""
Calculates an encoded salt string, including prefix, for this algorithm.
[Override]
"""

salt_chars = passlib.utils.getrandstr(passlib.utils.rng,
passlib.hash.django_pbkdf2_sha256.salt_chars,
c.salt_length)
s = "%s$%d$%s$" % (c.prefix, c.rounds, salt_chars)
return s


def __init__(self, salt):
super().__init__(salt)

## print(self.salt[self.salt_prefix_len:])
startidx = self.salt_prefix_len
endidx = self.salt_prefix_len + self.salt_length
info = { 'salt': self.salt[startidx:endidx],
'rounds': self.rounds }
self.hasher = passlib.hash.django_pbkdf2_sha256.using(**info)
13 changes: 13 additions & 0 deletions src/hashpw/algs/PhpBB3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from .Phpass import Phpass
from .. import errors


class PhpBB3(Phpass):
name = "phpBB3"
option = "B"
prefix = "$H$"
suffix = ""


def hash(self, plaintext):
raise errors.BadAlgException(self.name + " not implemented")
43 changes: 43 additions & 0 deletions src/hashpw/algs/Phpass.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import passlib.hash
import passlib.utils.binary

from ..structure import PLSaltedAlgorithm


class Phpass(PLSaltedAlgorithm):
"""https://github.com/exavolt/python-phpass
e.g. portable (safe MD5): $P$Bnvt73R2AZ9NwrY8agFUwI1YUYQEW5/
Blowfish: $2a$08$iys2/e7hwWyX2YbWtjCyY.tmGy2Y.mGlV9KwIAi9AUPgBuc9rdJVe"""

name = "phpass"
option = "P"
prefix = "$P$"
suffix = ""
min_length = 34
salt_length = 9 # includes the round count


@classmethod
def final_prep(c):
"""[Override]"""
c.rounds=17
## c.round_id_chars = "23456789ABCDEFGHIJKLMNOP"
## c.round_id_chars = "789ABCDEFGHIJKLMNOPQRSTU"

# Pass it up the hierarchy
PLSaltedAlgorithm.final_prep()


def __init__(self, salt):
super().__init__(salt)

self.hasher = passlib.hash.phpass.using(salt=self.salt[4:], rounds=self.rounds)


@classmethod
def generate_salt(c):
"""Calculates an encoded salt string, including prefix, for this algorithm."""
salt_chars = PLSaltedAlgorithm.generate_raw_salt()[0:8]
round_char = passlib.utils.binary.h64.encode_int6(c.rounds).decode("ascii")
s = c.prefix + round_char + salt_chars
return s
Loading

0 comments on commit 73cce0c

Please sign in to comment.