diff --git a/src/passwordlib/exceptions.py b/src/passwordlib/exceptions.py new file mode 100644 index 0000000..855b8ae --- /dev/null +++ b/src/passwordlib/exceptions.py @@ -0,0 +1,19 @@ +# -*- coding=utf-8 -*- +r""" + +""" + + +__all__ = ['PasswordlibError', 'PWLibSyntaxError', 'PWLibBadPrefixError'] + + +class PasswordlibError(Exception): + pass + + +class PWLibSyntaxError(PasswordlibError): + pass + + +class PWLibBadPrefixError(PasswordlibError): + pass diff --git a/src/passwordlib/unix/__init__.py b/src/passwordlib/unix/__init__.py new file mode 100644 index 0000000..7f1cda6 --- /dev/null +++ b/src/passwordlib/unix/__init__.py @@ -0,0 +1,6 @@ +# -*- coding=utf-8 -*- +r""" +Compatibility for the widely used unix formats +""" +from . import encrypt +from . import loading diff --git a/src/passwordlib/unix/encrypt.py b/src/passwordlib/unix/encrypt.py new file mode 100644 index 0000000..4fb1476 --- /dev/null +++ b/src/passwordlib/unix/encrypt.py @@ -0,0 +1,27 @@ +# -*- coding=utf-8 -*- +r""" + +""" +import typing as t +from ..core import functions as fn + + +__all__ = ['encrypt_bcrypt', 'encrypt_sha1'] + + +BCRYPT_PREFIX: t.TypeAlias = t.Union[t.Literal[b"2a"], t.Literal[b"2b"]] + + +def encrypt_bcrypt(password: t.AnyStr, salt: str = None, rounds: int = 12, prefix: BCRYPT_PREFIX = "2b") -> str: + import bcrypt + password = fn.get_password_bytes(password) + salt = salt or bcrypt.gensalt(rounds=rounds, prefix=prefix) + return bcrypt.hashpw(password, salt=salt).decode() + + +def encrypt_sha1(password: t.AnyStr) -> str: + from hashlib import sha1 + from base64 import b64encode + password = fn.get_password_bytes(password) + hashed = sha1(password).digest() + return "{SHA1}" + b64encode(hashed).decode() diff --git a/src/passwordlib/unix/loading.py b/src/passwordlib/unix/loading.py new file mode 100644 index 0000000..9a742d3 --- /dev/null +++ b/src/passwordlib/unix/loading.py @@ -0,0 +1,53 @@ +# -*- coding=utf-8 -*- +r""" + +""" +from ..core import LoadedTuple +from ..exceptions import * + + +__all__ = ['load_bcrypt', 'load_sha1'] + + +def load_bcrypt(dump: str) -> LoadedTuple: + parts = dump.split("$") + if len(parts) != 4: raise PWLibSyntaxError("bad format") + + void, prefix, rounds, salt_and_hash = parts + if void: raise PWLibSyntaxError("dump does not start with '$'") + if prefix not in {"2", "2a", "2b", "2x", "2y"}: raise PWLibBadPrefixError("unknown prefix") + if not rounds.isdigit(): raise PWLibSyntaxError("rounds are not an integer") + + iterations = int(rounds) + salt = salt_and_hash[:22].encode() # note: salt is in a base-64 encoded format + hashed = salt_and_hash[22:].encode() + return LoadedTuple(algorithm="bcrypt", iterations=iterations, salt=salt, hashed=hashed) + + +def load_sha256(dump: str) -> LoadedTuple: + parts = dump.split("$") + n_parts = len(parts) + if n_parts != 4 and n_parts != 5: raise PWLibSyntaxError("bad format") + + if n_parts == 4: + void, prefix, salt, checksum = parts + rounds = "rounds=5000" + else: + void, prefix, rounds, salt, checksum = parts + if void: raise PWLibSyntaxError("dump does not start with '$'") + if prefix != "5": raise PWLibSyntaxError("unknown prefix") + if not rounds.startswith("rounds=") or not rounds[7:].isdigit(): raise PWLibSyntaxError("bad rounds") + + iterations = int(rounds[7:]) + salt = salt.encode() + hashed = checksum.encode() + + return LoadedTuple(algorithm="sha256", iterations=iterations, salt=salt, hashed=hashed) + + +def load_sha1(dump: str) -> LoadedTuple: + from base64 import b64decode + if not dump.startswith("{SHA}"): raise PWLibBadPrefixError("Invalid dump format") + b64encoded = dump[6:] + hashed = b64decode(b64encoded) + return LoadedTuple(algorithm="sha1", iterations=-1, salt=b'', hashed=hashed) diff --git a/src/passwordlib/unix/utils.py b/src/passwordlib/unix/utils.py new file mode 100644 index 0000000..a8c4af3 --- /dev/null +++ b/src/passwordlib/unix/utils.py @@ -0,0 +1,18 @@ +# -*- coding=utf-8 -*- +r""" + +""" +import os +import typing as t + + +__all__ = ['gen_salt_str'] + + +def gen_salt_str(length: int, *, characters: t.Sequence[str]) -> str: + r""" + :param length: length of the resulting salt + :param characters: characters to use in the salt + :return: generated salt + """ + return ''.join(characters[int(r / 256 * len(characters))] for r in os.urandom(length))