first commit
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""passlib.handlers -- holds implementations of all passlib's builtin hash formats"""
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
1009
venv/lib/python3.12/site-packages/passlib/handlers/argon2.py
Normal file
1009
venv/lib/python3.12/site-packages/passlib/handlers/argon2.py
Normal file
File diff suppressed because it is too large
Load Diff
1243
venv/lib/python3.12/site-packages/passlib/handlers/bcrypt.py
Normal file
1243
venv/lib/python3.12/site-packages/passlib/handlers/bcrypt.py
Normal file
File diff suppressed because it is too large
Load Diff
440
venv/lib/python3.12/site-packages/passlib/handlers/cisco.py
Normal file
440
venv/lib/python3.12/site-packages/passlib/handlers/cisco.py
Normal file
@@ -0,0 +1,440 @@
|
||||
"""
|
||||
passlib.handlers.cisco -- Cisco password hashes
|
||||
"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
# core
|
||||
from binascii import hexlify, unhexlify
|
||||
from hashlib import md5
|
||||
import logging; log = logging.getLogger(__name__)
|
||||
from warnings import warn
|
||||
# site
|
||||
# pkg
|
||||
from passlib.utils import right_pad_string, to_unicode, repeat_string, to_bytes
|
||||
from passlib.utils.binary import h64
|
||||
from passlib.utils.compat import unicode, u, join_byte_values, \
|
||||
join_byte_elems, iter_byte_values, uascii_to_str
|
||||
import passlib.utils.handlers as uh
|
||||
# local
|
||||
__all__ = [
|
||||
"cisco_pix",
|
||||
"cisco_asa",
|
||||
"cisco_type7",
|
||||
]
|
||||
|
||||
#=============================================================================
|
||||
# utils
|
||||
#=============================================================================
|
||||
|
||||
#: dummy bytes used by spoil_digest var in cisco_pix._calc_checksum()
|
||||
_DUMMY_BYTES = b'\xFF' * 32
|
||||
|
||||
#=============================================================================
|
||||
# cisco pix firewall hash
|
||||
#=============================================================================
|
||||
class cisco_pix(uh.HasUserContext, uh.StaticHandler):
|
||||
"""
|
||||
This class implements the password hash used by older Cisco PIX firewalls,
|
||||
and follows the :ref:`password-hash-api`.
|
||||
It does a single round of hashing, and relies on the username
|
||||
as the salt.
|
||||
|
||||
This class only allows passwords <= 16 bytes, anything larger
|
||||
will result in a :exc:`~passlib.exc.PasswordSizeError` if passed to :meth:`~cisco_pix.hash`,
|
||||
and be silently rejected if passed to :meth:`~cisco_pix.verify`.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.hash`,
|
||||
:meth:`~passlib.ifc.PasswordHash.genhash`, and
|
||||
:meth:`~passlib.ifc.PasswordHash.verify` methods
|
||||
all support the following extra keyword:
|
||||
|
||||
:param str user:
|
||||
String containing name of user account this password is associated with.
|
||||
|
||||
This is *required* in order to correctly hash passwords associated
|
||||
with a user account on the Cisco device, as it is used to salt
|
||||
the hash.
|
||||
|
||||
Conversely, this *must* be omitted or set to ``""`` in order to correctly
|
||||
hash passwords which don't have an associated user account
|
||||
(such as the "enable" password).
|
||||
|
||||
.. versionadded:: 1.6
|
||||
|
||||
.. versionchanged:: 1.7.1
|
||||
|
||||
Passwords > 16 bytes are now rejected / throw error instead of being silently truncated,
|
||||
to match Cisco behavior. A number of :ref:`bugs <passlib-asa96-bug>` were fixed
|
||||
which caused prior releases to generate unverifiable hashes in certain cases.
|
||||
"""
|
||||
#===================================================================
|
||||
# class attrs
|
||||
#===================================================================
|
||||
|
||||
#--------------------
|
||||
# PasswordHash
|
||||
#--------------------
|
||||
name = "cisco_pix"
|
||||
|
||||
truncate_size = 16
|
||||
|
||||
# NOTE: these are the default policy for PasswordHash,
|
||||
# but want to set them explicitly for now.
|
||||
truncate_error = True
|
||||
truncate_verify_reject = True
|
||||
|
||||
#--------------------
|
||||
# GenericHandler
|
||||
#--------------------
|
||||
checksum_size = 16
|
||||
checksum_chars = uh.HASH64_CHARS
|
||||
|
||||
#--------------------
|
||||
# custom
|
||||
#--------------------
|
||||
|
||||
#: control flag signalling "cisco_asa" mode, set by cisco_asa class
|
||||
_is_asa = False
|
||||
|
||||
#===================================================================
|
||||
# methods
|
||||
#===================================================================
|
||||
def _calc_checksum(self, secret):
|
||||
"""
|
||||
This function implements the "encrypted" hash format used by Cisco
|
||||
PIX & ASA. It's behavior has been confirmed for ASA 9.6,
|
||||
but is presumed correct for PIX & other ASA releases,
|
||||
as it fits with known test vectors, and existing literature.
|
||||
|
||||
While nearly the same, the PIX & ASA hashes have slight differences,
|
||||
so this function performs differently based on the _is_asa class flag.
|
||||
Noteable changes from PIX to ASA include password size limit
|
||||
increased from 16 -> 32, and other internal changes.
|
||||
"""
|
||||
# select PIX vs or ASA mode
|
||||
asa = self._is_asa
|
||||
|
||||
#
|
||||
# encode secret
|
||||
#
|
||||
# per ASA 8.4 documentation,
|
||||
# http://www.cisco.com/c/en/us/td/docs/security/asa/asa84/configuration/guide/asa_84_cli_config/ref_cli.html#Supported_Character_Sets,
|
||||
# it supposedly uses UTF-8 -- though some double-encoding issues have
|
||||
# been observed when trying to actually *set* a non-ascii password
|
||||
# via ASDM, and access via SSH seems to strip 8-bit chars.
|
||||
#
|
||||
if isinstance(secret, unicode):
|
||||
secret = secret.encode("utf-8")
|
||||
|
||||
#
|
||||
# check if password too large
|
||||
#
|
||||
# Per ASA 9.6 changes listed in
|
||||
# http://www.cisco.com/c/en/us/td/docs/security/asa/roadmap/asa_new_features.html,
|
||||
# prior releases had a maximum limit of 32 characters.
|
||||
# Testing with an ASA 9.6 system bears this out --
|
||||
# setting 32-char password for a user account,
|
||||
# and logins will fail if any chars are appended.
|
||||
# (ASA 9.6 added new PBKDF2-based hash algorithm,
|
||||
# which supports larger passwords).
|
||||
#
|
||||
# Per PIX documentation
|
||||
# http://www.cisco.com/en/US/docs/security/pix/pix50/configuration/guide/commands.html,
|
||||
# it would not allow passwords > 16 chars.
|
||||
#
|
||||
# Thus, we unconditionally throw a password size error here,
|
||||
# as nothing valid can come from a larger password.
|
||||
# NOTE: assuming PIX has same behavior, but at 16 char limit.
|
||||
#
|
||||
spoil_digest = None
|
||||
if len(secret) > self.truncate_size:
|
||||
if self.use_defaults:
|
||||
# called from hash()
|
||||
msg = "Password too long (%s allows at most %d bytes)" % \
|
||||
(self.name, self.truncate_size)
|
||||
raise uh.exc.PasswordSizeError(self.truncate_size, msg=msg)
|
||||
else:
|
||||
# called from verify() --
|
||||
# We don't want to throw error, or return early,
|
||||
# as that would let attacker know too much. Instead, we set a
|
||||
# flag to add some dummy data into the md5 digest, so that
|
||||
# output won't match truncated version of secret, or anything
|
||||
# else that's fixed and predictable.
|
||||
spoil_digest = secret + _DUMMY_BYTES
|
||||
|
||||
#
|
||||
# append user to secret
|
||||
#
|
||||
# Policy appears to be:
|
||||
#
|
||||
# * Nothing appended for enable password (user = "")
|
||||
#
|
||||
# * ASA: If user present, but secret is >= 28 chars, nothing appended.
|
||||
#
|
||||
# * 1-2 byte users not allowed.
|
||||
# DEVIATION: we're letting them through, and repeating their
|
||||
# chars ala 3-char user, to simplify testing.
|
||||
# Could issue warning in the future though.
|
||||
#
|
||||
# * 3 byte user has first char repeated, to pad to 4.
|
||||
# (observed under ASA 9.6, assuming true elsewhere)
|
||||
#
|
||||
# * 4 byte users are used directly.
|
||||
#
|
||||
# * 5+ byte users are truncated to 4 bytes.
|
||||
#
|
||||
user = self.user
|
||||
if user:
|
||||
if isinstance(user, unicode):
|
||||
user = user.encode("utf-8")
|
||||
if not asa or len(secret) < 28:
|
||||
secret += repeat_string(user, 4)
|
||||
|
||||
#
|
||||
# pad / truncate result to limit
|
||||
#
|
||||
# While PIX always pads to 16 bytes, ASA increases to 32 bytes IFF
|
||||
# secret+user > 16 bytes. This makes PIX & ASA have different results
|
||||
# where secret size in range(13,16), and user is present --
|
||||
# PIX will truncate to 16, ASA will truncate to 32.
|
||||
#
|
||||
if asa and len(secret) > 16:
|
||||
pad_size = 32
|
||||
else:
|
||||
pad_size = 16
|
||||
secret = right_pad_string(secret, pad_size)
|
||||
|
||||
#
|
||||
# md5 digest
|
||||
#
|
||||
if spoil_digest:
|
||||
# make sure digest won't match truncated version of secret
|
||||
secret += spoil_digest
|
||||
digest = md5(secret).digest()
|
||||
|
||||
#
|
||||
# drop every 4th byte
|
||||
# NOTE: guessing this was done because it makes output exactly
|
||||
# 16 bytes, which may have been a general 'char password[]'
|
||||
# size limit under PIX
|
||||
#
|
||||
digest = join_byte_elems(c for i, c in enumerate(digest) if (i + 1) & 3)
|
||||
|
||||
#
|
||||
# encode using Hash64
|
||||
#
|
||||
return h64.encode_bytes(digest).decode("ascii")
|
||||
|
||||
# NOTE: works, but needs UTs.
|
||||
# @classmethod
|
||||
# def same_as_pix(cls, secret, user=""):
|
||||
# """
|
||||
# test whether (secret + user) combination should
|
||||
# have the same hash under PIX and ASA.
|
||||
#
|
||||
# mainly present to help unittests.
|
||||
# """
|
||||
# # see _calc_checksum() above for details of this logic.
|
||||
# size = len(to_bytes(secret, "utf-8"))
|
||||
# if user and size < 28:
|
||||
# size += 4
|
||||
# return size < 17
|
||||
|
||||
#===================================================================
|
||||
# eoc
|
||||
#===================================================================
|
||||
|
||||
|
||||
class cisco_asa(cisco_pix):
|
||||
"""
|
||||
This class implements the password hash used by Cisco ASA/PIX 7.0 and newer (2005).
|
||||
Aside from a different internal algorithm, it's use and format is identical
|
||||
to the older :class:`cisco_pix` class.
|
||||
|
||||
For passwords less than 13 characters, this should be identical to :class:`!cisco_pix`,
|
||||
but will generate a different hash for most larger inputs
|
||||
(See the `Format & Algorithm`_ section for the details).
|
||||
|
||||
This class only allows passwords <= 32 bytes, anything larger
|
||||
will result in a :exc:`~passlib.exc.PasswordSizeError` if passed to :meth:`~cisco_asa.hash`,
|
||||
and be silently rejected if passed to :meth:`~cisco_asa.verify`.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
.. versionchanged:: 1.7.1
|
||||
|
||||
Passwords > 32 bytes are now rejected / throw error instead of being silently truncated,
|
||||
to match Cisco behavior. A number of :ref:`bugs <passlib-asa96-bug>` were fixed
|
||||
which caused prior releases to generate unverifiable hashes in certain cases.
|
||||
"""
|
||||
#===================================================================
|
||||
# class attrs
|
||||
#===================================================================
|
||||
|
||||
#--------------------
|
||||
# PasswordHash
|
||||
#--------------------
|
||||
name = "cisco_asa"
|
||||
|
||||
#--------------------
|
||||
# TruncateMixin
|
||||
#--------------------
|
||||
truncate_size = 32
|
||||
|
||||
#--------------------
|
||||
# cisco_pix
|
||||
#--------------------
|
||||
_is_asa = True
|
||||
|
||||
#===================================================================
|
||||
# eoc
|
||||
#===================================================================
|
||||
|
||||
#=============================================================================
|
||||
# type 7
|
||||
#=============================================================================
|
||||
class cisco_type7(uh.GenericHandler):
|
||||
"""
|
||||
This class implements the "Type 7" password encoding used by Cisco IOS,
|
||||
and follows the :ref:`password-hash-api`.
|
||||
It has a simple 4-5 bit salt, but is nonetheless a reversible encoding
|
||||
instead of a real hash.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
|
||||
|
||||
:type salt: int
|
||||
:param salt:
|
||||
This may be an optional salt integer drawn from ``range(0,16)``.
|
||||
If omitted, one will be chosen at random.
|
||||
|
||||
:type relaxed: bool
|
||||
:param relaxed:
|
||||
By default, providing an invalid value for one of the other
|
||||
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
|
||||
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
|
||||
will be issued instead. Correctable errors include
|
||||
``salt`` values that are out of range.
|
||||
|
||||
Note that while this class outputs digests in upper-case hexadecimal,
|
||||
it will accept lower-case as well.
|
||||
|
||||
This class also provides the following additional method:
|
||||
|
||||
.. automethod:: decode
|
||||
"""
|
||||
#===================================================================
|
||||
# class attrs
|
||||
#===================================================================
|
||||
|
||||
#--------------------
|
||||
# PasswordHash
|
||||
#--------------------
|
||||
name = "cisco_type7"
|
||||
setting_kwds = ("salt",)
|
||||
|
||||
#--------------------
|
||||
# GenericHandler
|
||||
#--------------------
|
||||
checksum_chars = uh.UPPER_HEX_CHARS
|
||||
|
||||
#--------------------
|
||||
# HasSalt
|
||||
#--------------------
|
||||
|
||||
# NOTE: encoding could handle max_salt_value=99, but since key is only 52
|
||||
# chars in size, not sure what appropriate behavior is for that edge case.
|
||||
min_salt_value = 0
|
||||
max_salt_value = 52
|
||||
|
||||
#===================================================================
|
||||
# methods
|
||||
#===================================================================
|
||||
@classmethod
|
||||
def using(cls, salt=None, **kwds):
|
||||
subcls = super(cisco_type7, cls).using(**kwds)
|
||||
if salt is not None:
|
||||
salt = subcls._norm_salt(salt, relaxed=kwds.get("relaxed"))
|
||||
subcls._generate_salt = staticmethod(lambda: salt)
|
||||
return subcls
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, hash):
|
||||
hash = to_unicode(hash, "ascii", "hash")
|
||||
if len(hash) < 2:
|
||||
raise uh.exc.InvalidHashError(cls)
|
||||
salt = int(hash[:2]) # may throw ValueError
|
||||
return cls(salt=salt, checksum=hash[2:].upper())
|
||||
|
||||
def __init__(self, salt=None, **kwds):
|
||||
super(cisco_type7, self).__init__(**kwds)
|
||||
if salt is not None:
|
||||
salt = self._norm_salt(salt)
|
||||
elif self.use_defaults:
|
||||
salt = self._generate_salt()
|
||||
assert self._norm_salt(salt) == salt, "generated invalid salt: %r" % (salt,)
|
||||
else:
|
||||
raise TypeError("no salt specified")
|
||||
self.salt = salt
|
||||
|
||||
@classmethod
|
||||
def _norm_salt(cls, salt, relaxed=False):
|
||||
"""
|
||||
validate & normalize salt value.
|
||||
.. note::
|
||||
the salt for this algorithm is an integer 0-52, not a string
|
||||
"""
|
||||
if not isinstance(salt, int):
|
||||
raise uh.exc.ExpectedTypeError(salt, "integer", "salt")
|
||||
if 0 <= salt <= cls.max_salt_value:
|
||||
return salt
|
||||
msg = "salt/offset must be in 0..52 range"
|
||||
if relaxed:
|
||||
warn(msg, uh.PasslibHashWarning)
|
||||
return 0 if salt < 0 else cls.max_salt_value
|
||||
else:
|
||||
raise ValueError(msg)
|
||||
|
||||
@staticmethod
|
||||
def _generate_salt():
|
||||
return uh.rng.randint(0, 15)
|
||||
|
||||
def to_string(self):
|
||||
return "%02d%s" % (self.salt, uascii_to_str(self.checksum))
|
||||
|
||||
def _calc_checksum(self, secret):
|
||||
# XXX: no idea what unicode policy is, but all examples are
|
||||
# 7-bit ascii compatible, so using UTF-8
|
||||
if isinstance(secret, unicode):
|
||||
secret = secret.encode("utf-8")
|
||||
return hexlify(self._cipher(secret, self.salt)).decode("ascii").upper()
|
||||
|
||||
@classmethod
|
||||
def decode(cls, hash, encoding="utf-8"):
|
||||
"""decode hash, returning original password.
|
||||
|
||||
:arg hash: encoded password
|
||||
:param encoding: optional encoding to use (defaults to ``UTF-8``).
|
||||
:returns: password as unicode
|
||||
"""
|
||||
self = cls.from_string(hash)
|
||||
tmp = unhexlify(self.checksum.encode("ascii"))
|
||||
raw = self._cipher(tmp, self.salt)
|
||||
return raw.decode(encoding) if encoding else raw
|
||||
|
||||
# type7 uses a xor-based vingere variant, using the following secret key:
|
||||
_key = u("dsfd;kfoA,.iyewrkldJKDHSUBsgvca69834ncxv9873254k;fg87")
|
||||
|
||||
@classmethod
|
||||
def _cipher(cls, data, salt):
|
||||
"""xor static key against data - encrypts & decrypts"""
|
||||
key = cls._key
|
||||
key_size = len(key)
|
||||
return join_byte_values(
|
||||
value ^ ord(key[(salt + idx) % key_size])
|
||||
for idx, value in enumerate(iter_byte_values(data))
|
||||
)
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
607
venv/lib/python3.12/site-packages/passlib/handlers/des_crypt.py
Normal file
607
venv/lib/python3.12/site-packages/passlib/handlers/des_crypt.py
Normal file
@@ -0,0 +1,607 @@
|
||||
"""passlib.handlers.des_crypt - traditional unix (DES) crypt and variants"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
# core
|
||||
import re
|
||||
import logging; log = logging.getLogger(__name__)
|
||||
from warnings import warn
|
||||
# site
|
||||
# pkg
|
||||
from passlib.utils import safe_crypt, test_crypt, to_unicode
|
||||
from passlib.utils.binary import h64, h64big
|
||||
from passlib.utils.compat import byte_elem_value, u, uascii_to_str, unicode, suppress_cause
|
||||
from passlib.crypto.des import des_encrypt_int_block
|
||||
import passlib.utils.handlers as uh
|
||||
# local
|
||||
__all__ = [
|
||||
"des_crypt",
|
||||
"bsdi_crypt",
|
||||
"bigcrypt",
|
||||
"crypt16",
|
||||
]
|
||||
|
||||
#=============================================================================
|
||||
# pure-python backend for des_crypt family
|
||||
#=============================================================================
|
||||
_BNULL = b'\x00'
|
||||
|
||||
def _crypt_secret_to_key(secret):
|
||||
"""convert secret to 64-bit DES key.
|
||||
|
||||
this only uses the first 8 bytes of the secret,
|
||||
and discards the high 8th bit of each byte at that.
|
||||
a null parity bit is inserted after every 7th bit of the output.
|
||||
"""
|
||||
# NOTE: this would set the parity bits correctly,
|
||||
# but des_encrypt_int_block() would just ignore them...
|
||||
##return sum(expand_7bit(byte_elem_value(c) & 0x7f) << (56-i*8)
|
||||
## for i, c in enumerate(secret[:8]))
|
||||
return sum((byte_elem_value(c) & 0x7f) << (57-i*8)
|
||||
for i, c in enumerate(secret[:8]))
|
||||
|
||||
def _raw_des_crypt(secret, salt):
|
||||
"""pure-python backed for des_crypt"""
|
||||
assert len(salt) == 2
|
||||
|
||||
# NOTE: some OSes will accept non-HASH64 characters in the salt,
|
||||
# but what value they assign these characters varies wildy,
|
||||
# so just rejecting them outright.
|
||||
# the same goes for single-character salts...
|
||||
# some OSes duplicate the char, some insert a '.' char,
|
||||
# and openbsd does (something) which creates an invalid hash.
|
||||
salt_value = h64.decode_int12(salt)
|
||||
|
||||
# gotta do something - no official policy since this predates unicode
|
||||
if isinstance(secret, unicode):
|
||||
secret = secret.encode("utf-8")
|
||||
assert isinstance(secret, bytes)
|
||||
|
||||
# forbidding NULL char because underlying crypt() rejects them too.
|
||||
if _BNULL in secret:
|
||||
raise uh.exc.NullPasswordError(des_crypt)
|
||||
|
||||
# convert first 8 bytes of secret string into an integer
|
||||
key_value = _crypt_secret_to_key(secret)
|
||||
|
||||
# run data through des using input of 0
|
||||
result = des_encrypt_int_block(key_value, 0, salt_value, 25)
|
||||
|
||||
# run h64 encode on result
|
||||
return h64big.encode_int64(result)
|
||||
|
||||
def _bsdi_secret_to_key(secret):
|
||||
"""convert secret to DES key used by bsdi_crypt"""
|
||||
key_value = _crypt_secret_to_key(secret)
|
||||
idx = 8
|
||||
end = len(secret)
|
||||
while idx < end:
|
||||
next = idx + 8
|
||||
tmp_value = _crypt_secret_to_key(secret[idx:next])
|
||||
key_value = des_encrypt_int_block(key_value, key_value) ^ tmp_value
|
||||
idx = next
|
||||
return key_value
|
||||
|
||||
def _raw_bsdi_crypt(secret, rounds, salt):
|
||||
"""pure-python backend for bsdi_crypt"""
|
||||
|
||||
# decode salt
|
||||
salt_value = h64.decode_int24(salt)
|
||||
|
||||
# gotta do something - no official policy since this predates unicode
|
||||
if isinstance(secret, unicode):
|
||||
secret = secret.encode("utf-8")
|
||||
assert isinstance(secret, bytes)
|
||||
|
||||
# forbidding NULL char because underlying crypt() rejects them too.
|
||||
if _BNULL in secret:
|
||||
raise uh.exc.NullPasswordError(bsdi_crypt)
|
||||
|
||||
# convert secret string into an integer
|
||||
key_value = _bsdi_secret_to_key(secret)
|
||||
|
||||
# run data through des using input of 0
|
||||
result = des_encrypt_int_block(key_value, 0, salt_value, rounds)
|
||||
|
||||
# run h64 encode on result
|
||||
return h64big.encode_int64(result)
|
||||
|
||||
#=============================================================================
|
||||
# handlers
|
||||
#=============================================================================
|
||||
class des_crypt(uh.TruncateMixin, uh.HasManyBackends, uh.HasSalt, uh.GenericHandler):
|
||||
"""This class implements the des-crypt password hash, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports a fixed-length salt.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
|
||||
|
||||
:type salt: str
|
||||
:param salt:
|
||||
Optional salt string.
|
||||
If not specified, one will be autogenerated (this is recommended).
|
||||
If specified, it must be 2 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
|
||||
|
||||
:param bool truncate_error:
|
||||
By default, des_crypt will silently truncate passwords larger than 8 bytes.
|
||||
Setting ``truncate_error=True`` will cause :meth:`~passlib.ifc.PasswordHash.hash`
|
||||
to raise a :exc:`~passlib.exc.PasswordTruncateError` instead.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
:type relaxed: bool
|
||||
:param relaxed:
|
||||
By default, providing an invalid value for one of the other
|
||||
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
|
||||
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
|
||||
will be issued instead. Correctable errors include
|
||||
``salt`` strings that are too long.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
"""
|
||||
#===================================================================
|
||||
# class attrs
|
||||
#===================================================================
|
||||
|
||||
#--------------------
|
||||
# PasswordHash
|
||||
#--------------------
|
||||
name = "des_crypt"
|
||||
setting_kwds = ("salt", "truncate_error")
|
||||
|
||||
#--------------------
|
||||
# GenericHandler
|
||||
#--------------------
|
||||
checksum_chars = uh.HASH64_CHARS
|
||||
checksum_size = 11
|
||||
|
||||
#--------------------
|
||||
# HasSalt
|
||||
#--------------------
|
||||
min_salt_size = max_salt_size = 2
|
||||
salt_chars = uh.HASH64_CHARS
|
||||
|
||||
#--------------------
|
||||
# TruncateMixin
|
||||
#--------------------
|
||||
truncate_size = 8
|
||||
|
||||
#===================================================================
|
||||
# formatting
|
||||
#===================================================================
|
||||
# FORMAT: 2 chars of H64-encoded salt + 11 chars of H64-encoded checksum
|
||||
|
||||
_hash_regex = re.compile(u(r"""
|
||||
^
|
||||
(?P<salt>[./a-z0-9]{2})
|
||||
(?P<chk>[./a-z0-9]{11})?
|
||||
$"""), re.X|re.I)
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, hash):
|
||||
hash = to_unicode(hash, "ascii", "hash")
|
||||
salt, chk = hash[:2], hash[2:]
|
||||
return cls(salt=salt, checksum=chk or None)
|
||||
|
||||
def to_string(self):
|
||||
hash = u("%s%s") % (self.salt, self.checksum)
|
||||
return uascii_to_str(hash)
|
||||
|
||||
#===================================================================
|
||||
# digest calculation
|
||||
#===================================================================
|
||||
def _calc_checksum(self, secret):
|
||||
# check for truncation (during .hash() calls only)
|
||||
if self.use_defaults:
|
||||
self._check_truncate_policy(secret)
|
||||
|
||||
return self._calc_checksum_backend(secret)
|
||||
|
||||
#===================================================================
|
||||
# backend
|
||||
#===================================================================
|
||||
backends = ("os_crypt", "builtin")
|
||||
|
||||
#---------------------------------------------------------------
|
||||
# os_crypt backend
|
||||
#---------------------------------------------------------------
|
||||
@classmethod
|
||||
def _load_backend_os_crypt(cls):
|
||||
if test_crypt("test", 'abgOeLfPimXQo'):
|
||||
cls._set_calc_checksum_backend(cls._calc_checksum_os_crypt)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def _calc_checksum_os_crypt(self, secret):
|
||||
# NOTE: we let safe_crypt() encode unicode secret -> utf8;
|
||||
# no official policy since des-crypt predates unicode
|
||||
hash = safe_crypt(secret, self.salt)
|
||||
if hash is None:
|
||||
# py3's crypt.crypt() can't handle non-utf8 bytes.
|
||||
# fallback to builtin alg, which is always available.
|
||||
return self._calc_checksum_builtin(secret)
|
||||
if not hash.startswith(self.salt) or len(hash) != 13:
|
||||
raise uh.exc.CryptBackendError(self, self.salt, hash)
|
||||
return hash[2:]
|
||||
|
||||
#---------------------------------------------------------------
|
||||
# builtin backend
|
||||
#---------------------------------------------------------------
|
||||
@classmethod
|
||||
def _load_backend_builtin(cls):
|
||||
cls._set_calc_checksum_backend(cls._calc_checksum_builtin)
|
||||
return True
|
||||
|
||||
def _calc_checksum_builtin(self, secret):
|
||||
return _raw_des_crypt(secret, self.salt.encode("ascii")).decode("ascii")
|
||||
|
||||
#===================================================================
|
||||
# eoc
|
||||
#===================================================================
|
||||
|
||||
class bsdi_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler):
|
||||
"""This class implements the BSDi-Crypt password hash, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports a fixed-length salt, and a variable number of rounds.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
|
||||
|
||||
:type salt: str
|
||||
:param salt:
|
||||
Optional salt string.
|
||||
If not specified, one will be autogenerated (this is recommended).
|
||||
If specified, it must be 4 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
|
||||
|
||||
:type rounds: int
|
||||
:param rounds:
|
||||
Optional number of rounds to use.
|
||||
Defaults to 5001, must be between 1 and 16777215, inclusive.
|
||||
|
||||
:type relaxed: bool
|
||||
:param relaxed:
|
||||
By default, providing an invalid value for one of the other
|
||||
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
|
||||
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
|
||||
will be issued instead. Correctable errors include ``rounds``
|
||||
that are too small or too large, and ``salt`` strings that are too long.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
|
||||
.. versionchanged:: 1.6
|
||||
:meth:`hash` will now issue a warning if an even number of rounds is used
|
||||
(see :ref:`bsdi-crypt-security-issues` regarding weak DES keys).
|
||||
"""
|
||||
#===================================================================
|
||||
# class attrs
|
||||
#===================================================================
|
||||
#--GenericHandler--
|
||||
name = "bsdi_crypt"
|
||||
setting_kwds = ("salt", "rounds")
|
||||
checksum_size = 11
|
||||
checksum_chars = uh.HASH64_CHARS
|
||||
|
||||
#--HasSalt--
|
||||
min_salt_size = max_salt_size = 4
|
||||
salt_chars = uh.HASH64_CHARS
|
||||
|
||||
#--HasRounds--
|
||||
default_rounds = 5001
|
||||
min_rounds = 1
|
||||
max_rounds = 16777215 # (1<<24)-1
|
||||
rounds_cost = "linear"
|
||||
|
||||
# NOTE: OpenBSD login.conf reports 7250 as minimum allowed rounds,
|
||||
# but that seems to be an OS policy, not a algorithm limitation.
|
||||
|
||||
#===================================================================
|
||||
# parsing
|
||||
#===================================================================
|
||||
_hash_regex = re.compile(u(r"""
|
||||
^
|
||||
_
|
||||
(?P<rounds>[./a-z0-9]{4})
|
||||
(?P<salt>[./a-z0-9]{4})
|
||||
(?P<chk>[./a-z0-9]{11})?
|
||||
$"""), re.X|re.I)
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, hash):
|
||||
hash = to_unicode(hash, "ascii", "hash")
|
||||
m = cls._hash_regex.match(hash)
|
||||
if not m:
|
||||
raise uh.exc.InvalidHashError(cls)
|
||||
rounds, salt, chk = m.group("rounds", "salt", "chk")
|
||||
return cls(
|
||||
rounds=h64.decode_int24(rounds.encode("ascii")),
|
||||
salt=salt,
|
||||
checksum=chk,
|
||||
)
|
||||
|
||||
def to_string(self):
|
||||
hash = u("_%s%s%s") % (h64.encode_int24(self.rounds).decode("ascii"),
|
||||
self.salt, self.checksum)
|
||||
return uascii_to_str(hash)
|
||||
|
||||
#===================================================================
|
||||
# validation
|
||||
#===================================================================
|
||||
|
||||
# NOTE: keeping this flag for admin/choose_rounds.py script.
|
||||
# want to eventually expose rounds logic to that script in better way.
|
||||
_avoid_even_rounds = True
|
||||
|
||||
@classmethod
|
||||
def using(cls, **kwds):
|
||||
subcls = super(bsdi_crypt, cls).using(**kwds)
|
||||
if not subcls.default_rounds & 1:
|
||||
# issue warning if caller set an even 'rounds' value.
|
||||
warn("bsdi_crypt rounds should be odd, as even rounds may reveal weak DES keys",
|
||||
uh.exc.PasslibSecurityWarning)
|
||||
return subcls
|
||||
|
||||
@classmethod
|
||||
def _generate_rounds(cls):
|
||||
rounds = super(bsdi_crypt, cls)._generate_rounds()
|
||||
# ensure autogenerated rounds are always odd
|
||||
# NOTE: doing this even for default_rounds so needs_update() doesn't get
|
||||
# caught in a loop.
|
||||
# FIXME: this technically might generate a rounds value 1 larger
|
||||
# than the requested upper bound - but better to err on side of safety.
|
||||
return rounds|1
|
||||
|
||||
#===================================================================
|
||||
# migration
|
||||
#===================================================================
|
||||
|
||||
def _calc_needs_update(self, **kwds):
|
||||
# mark bsdi_crypt hashes as deprecated if they have even rounds.
|
||||
if not self.rounds & 1:
|
||||
return True
|
||||
# hand off to base implementation
|
||||
return super(bsdi_crypt, self)._calc_needs_update(**kwds)
|
||||
|
||||
#===================================================================
|
||||
# backends
|
||||
#===================================================================
|
||||
backends = ("os_crypt", "builtin")
|
||||
|
||||
#---------------------------------------------------------------
|
||||
# os_crypt backend
|
||||
#---------------------------------------------------------------
|
||||
@classmethod
|
||||
def _load_backend_os_crypt(cls):
|
||||
if test_crypt("test", '_/...lLDAxARksGCHin.'):
|
||||
cls._set_calc_checksum_backend(cls._calc_checksum_os_crypt)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def _calc_checksum_os_crypt(self, secret):
|
||||
config = self.to_string()
|
||||
hash = safe_crypt(secret, config)
|
||||
if hash is None:
|
||||
# py3's crypt.crypt() can't handle non-utf8 bytes.
|
||||
# fallback to builtin alg, which is always available.
|
||||
return self._calc_checksum_builtin(secret)
|
||||
if not hash.startswith(config[:9]) or len(hash) != 20:
|
||||
raise uh.exc.CryptBackendError(self, config, hash)
|
||||
return hash[-11:]
|
||||
|
||||
#---------------------------------------------------------------
|
||||
# builtin backend
|
||||
#---------------------------------------------------------------
|
||||
@classmethod
|
||||
def _load_backend_builtin(cls):
|
||||
cls._set_calc_checksum_backend(cls._calc_checksum_builtin)
|
||||
return True
|
||||
|
||||
def _calc_checksum_builtin(self, secret):
|
||||
return _raw_bsdi_crypt(secret, self.rounds, self.salt.encode("ascii")).decode("ascii")
|
||||
|
||||
#===================================================================
|
||||
# eoc
|
||||
#===================================================================
|
||||
|
||||
class bigcrypt(uh.HasSalt, uh.GenericHandler):
|
||||
"""This class implements the BigCrypt password hash, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports a fixed-length salt.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
|
||||
|
||||
:type salt: str
|
||||
:param salt:
|
||||
Optional salt string.
|
||||
If not specified, one will be autogenerated (this is recommended).
|
||||
If specified, it must be 22 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
|
||||
|
||||
:type relaxed: bool
|
||||
:param relaxed:
|
||||
By default, providing an invalid value for one of the other
|
||||
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
|
||||
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
|
||||
will be issued instead. Correctable errors include
|
||||
``salt`` strings that are too long.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
"""
|
||||
#===================================================================
|
||||
# class attrs
|
||||
#===================================================================
|
||||
#--GenericHandler--
|
||||
name = "bigcrypt"
|
||||
setting_kwds = ("salt",)
|
||||
checksum_chars = uh.HASH64_CHARS
|
||||
# NOTE: checksum chars must be multiple of 11
|
||||
|
||||
#--HasSalt--
|
||||
min_salt_size = max_salt_size = 2
|
||||
salt_chars = uh.HASH64_CHARS
|
||||
|
||||
#===================================================================
|
||||
# internal helpers
|
||||
#===================================================================
|
||||
_hash_regex = re.compile(u(r"""
|
||||
^
|
||||
(?P<salt>[./a-z0-9]{2})
|
||||
(?P<chk>([./a-z0-9]{11})+)?
|
||||
$"""), re.X|re.I)
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, hash):
|
||||
hash = to_unicode(hash, "ascii", "hash")
|
||||
m = cls._hash_regex.match(hash)
|
||||
if not m:
|
||||
raise uh.exc.InvalidHashError(cls)
|
||||
salt, chk = m.group("salt", "chk")
|
||||
return cls(salt=salt, checksum=chk)
|
||||
|
||||
def to_string(self):
|
||||
hash = u("%s%s") % (self.salt, self.checksum)
|
||||
return uascii_to_str(hash)
|
||||
|
||||
def _norm_checksum(self, checksum, relaxed=False):
|
||||
checksum = super(bigcrypt, self)._norm_checksum(checksum, relaxed=relaxed)
|
||||
if len(checksum) % 11:
|
||||
raise uh.exc.InvalidHashError(self)
|
||||
return checksum
|
||||
|
||||
#===================================================================
|
||||
# backend
|
||||
#===================================================================
|
||||
def _calc_checksum(self, secret):
|
||||
if isinstance(secret, unicode):
|
||||
secret = secret.encode("utf-8")
|
||||
chk = _raw_des_crypt(secret, self.salt.encode("ascii"))
|
||||
idx = 8
|
||||
end = len(secret)
|
||||
while idx < end:
|
||||
next = idx + 8
|
||||
chk += _raw_des_crypt(secret[idx:next], chk[-11:-9])
|
||||
idx = next
|
||||
return chk.decode("ascii")
|
||||
|
||||
#===================================================================
|
||||
# eoc
|
||||
#===================================================================
|
||||
|
||||
class crypt16(uh.TruncateMixin, uh.HasSalt, uh.GenericHandler):
|
||||
"""This class implements the crypt16 password hash, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports a fixed-length salt.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
|
||||
|
||||
:type salt: str
|
||||
:param salt:
|
||||
Optional salt string.
|
||||
If not specified, one will be autogenerated (this is recommended).
|
||||
If specified, it must be 2 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
|
||||
|
||||
:param bool truncate_error:
|
||||
By default, crypt16 will silently truncate passwords larger than 16 bytes.
|
||||
Setting ``truncate_error=True`` will cause :meth:`~passlib.ifc.PasswordHash.hash`
|
||||
to raise a :exc:`~passlib.exc.PasswordTruncateError` instead.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
:type relaxed: bool
|
||||
:param relaxed:
|
||||
By default, providing an invalid value for one of the other
|
||||
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
|
||||
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
|
||||
will be issued instead. Correctable errors include
|
||||
``salt`` strings that are too long.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
"""
|
||||
#===================================================================
|
||||
# class attrs
|
||||
#===================================================================
|
||||
|
||||
#--------------------
|
||||
# PasswordHash
|
||||
#--------------------
|
||||
name = "crypt16"
|
||||
setting_kwds = ("salt", "truncate_error")
|
||||
|
||||
#--------------------
|
||||
# GenericHandler
|
||||
#--------------------
|
||||
checksum_size = 22
|
||||
checksum_chars = uh.HASH64_CHARS
|
||||
|
||||
#--------------------
|
||||
# HasSalt
|
||||
#--------------------
|
||||
min_salt_size = max_salt_size = 2
|
||||
salt_chars = uh.HASH64_CHARS
|
||||
|
||||
#--------------------
|
||||
# TruncateMixin
|
||||
#--------------------
|
||||
truncate_size = 16
|
||||
|
||||
#===================================================================
|
||||
# internal helpers
|
||||
#===================================================================
|
||||
_hash_regex = re.compile(u(r"""
|
||||
^
|
||||
(?P<salt>[./a-z0-9]{2})
|
||||
(?P<chk>[./a-z0-9]{22})?
|
||||
$"""), re.X|re.I)
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, hash):
|
||||
hash = to_unicode(hash, "ascii", "hash")
|
||||
m = cls._hash_regex.match(hash)
|
||||
if not m:
|
||||
raise uh.exc.InvalidHashError(cls)
|
||||
salt, chk = m.group("salt", "chk")
|
||||
return cls(salt=salt, checksum=chk)
|
||||
|
||||
def to_string(self):
|
||||
hash = u("%s%s") % (self.salt, self.checksum)
|
||||
return uascii_to_str(hash)
|
||||
|
||||
#===================================================================
|
||||
# backend
|
||||
#===================================================================
|
||||
def _calc_checksum(self, secret):
|
||||
if isinstance(secret, unicode):
|
||||
secret = secret.encode("utf-8")
|
||||
|
||||
# check for truncation (during .hash() calls only)
|
||||
if self.use_defaults:
|
||||
self._check_truncate_policy(secret)
|
||||
|
||||
# parse salt value
|
||||
try:
|
||||
salt_value = h64.decode_int12(self.salt.encode("ascii"))
|
||||
except ValueError: # pragma: no cover - caught by class
|
||||
raise suppress_cause(ValueError("invalid chars in salt"))
|
||||
|
||||
# convert first 8 byts of secret string into an integer,
|
||||
key1 = _crypt_secret_to_key(secret)
|
||||
|
||||
# run data through des using input of 0
|
||||
result1 = des_encrypt_int_block(key1, 0, salt_value, 20)
|
||||
|
||||
# convert next 8 bytes of secret string into integer (key=0 if secret < 8 chars)
|
||||
key2 = _crypt_secret_to_key(secret[8:16])
|
||||
|
||||
# run data through des using input of 0
|
||||
result2 = des_encrypt_int_block(key2, 0, salt_value, 5)
|
||||
|
||||
# done
|
||||
chk = h64big.encode_int64(result1) + h64big.encode_int64(result2)
|
||||
return chk.decode("ascii")
|
||||
|
||||
#===================================================================
|
||||
# eoc
|
||||
#===================================================================
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
168
venv/lib/python3.12/site-packages/passlib/handlers/digests.py
Normal file
168
venv/lib/python3.12/site-packages/passlib/handlers/digests.py
Normal file
@@ -0,0 +1,168 @@
|
||||
"""passlib.handlers.digests - plain hash digests
|
||||
"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
# core
|
||||
import hashlib
|
||||
import logging; log = logging.getLogger(__name__)
|
||||
# site
|
||||
# pkg
|
||||
from passlib.utils import to_native_str, to_bytes, render_bytes, consteq
|
||||
from passlib.utils.compat import unicode, str_to_uascii
|
||||
import passlib.utils.handlers as uh
|
||||
from passlib.crypto.digest import lookup_hash
|
||||
# local
|
||||
__all__ = [
|
||||
"create_hex_hash",
|
||||
"hex_md4",
|
||||
"hex_md5",
|
||||
"hex_sha1",
|
||||
"hex_sha256",
|
||||
"hex_sha512",
|
||||
]
|
||||
|
||||
#=============================================================================
|
||||
# helpers for hexadecimal hashes
|
||||
#=============================================================================
|
||||
class HexDigestHash(uh.StaticHandler):
|
||||
"""this provides a template for supporting passwords stored as plain hexadecimal hashes"""
|
||||
#===================================================================
|
||||
# class attrs
|
||||
#===================================================================
|
||||
_hash_func = None # hash function to use - filled in by create_hex_hash()
|
||||
checksum_size = None # filled in by create_hex_hash()
|
||||
checksum_chars = uh.HEX_CHARS
|
||||
|
||||
#: special for detecting if _hash_func is just a stub method.
|
||||
supported = True
|
||||
|
||||
#===================================================================
|
||||
# methods
|
||||
#===================================================================
|
||||
@classmethod
|
||||
def _norm_hash(cls, hash):
|
||||
return hash.lower()
|
||||
|
||||
def _calc_checksum(self, secret):
|
||||
if isinstance(secret, unicode):
|
||||
secret = secret.encode("utf-8")
|
||||
return str_to_uascii(self._hash_func(secret).hexdigest())
|
||||
|
||||
#===================================================================
|
||||
# eoc
|
||||
#===================================================================
|
||||
|
||||
def create_hex_hash(digest, module=__name__, django_name=None, required=True):
|
||||
"""
|
||||
create hex-encoded unsalted hasher for specified digest algorithm.
|
||||
|
||||
.. versionchanged:: 1.7.3
|
||||
If called with unknown/supported digest, won't throw error immediately,
|
||||
but instead return a dummy hasher that will throw error when called.
|
||||
|
||||
set ``required=True`` to restore old behavior.
|
||||
"""
|
||||
info = lookup_hash(digest, required=required)
|
||||
name = "hex_" + info.name
|
||||
if not info.supported:
|
||||
info.digest_size = 0
|
||||
hasher = type(name, (HexDigestHash,), dict(
|
||||
name=name,
|
||||
__module__=module, # so ABCMeta won't clobber it
|
||||
_hash_func=staticmethod(info.const), # sometimes it's a function, sometimes not. so wrap it.
|
||||
checksum_size=info.digest_size*2,
|
||||
__doc__="""This class implements a plain hexadecimal %s hash, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports no optional or contextual keywords.
|
||||
""" % (info.name,)
|
||||
))
|
||||
if not info.supported:
|
||||
hasher.supported = False
|
||||
if django_name:
|
||||
hasher.django_name = django_name
|
||||
return hasher
|
||||
|
||||
#=============================================================================
|
||||
# predefined handlers
|
||||
#=============================================================================
|
||||
|
||||
# NOTE: some digests below are marked as "required=False", because these may not be present on
|
||||
# FIPS systems (see issue 116). if missing, will return stub hasher that throws error
|
||||
# if an attempt is made to actually use hash/verify with them.
|
||||
|
||||
hex_md4 = create_hex_hash("md4", required=False)
|
||||
hex_md5 = create_hex_hash("md5", django_name="unsalted_md5", required=False)
|
||||
hex_sha1 = create_hex_hash("sha1", required=False)
|
||||
hex_sha256 = create_hex_hash("sha256")
|
||||
hex_sha512 = create_hex_hash("sha512")
|
||||
|
||||
#=============================================================================
|
||||
# htdigest
|
||||
#=============================================================================
|
||||
class htdigest(uh.MinimalHandler):
|
||||
"""htdigest hash function.
|
||||
|
||||
.. todo::
|
||||
document this hash
|
||||
"""
|
||||
name = "htdigest"
|
||||
setting_kwds = ()
|
||||
context_kwds = ("user", "realm", "encoding")
|
||||
default_encoding = "utf-8"
|
||||
|
||||
@classmethod
|
||||
def hash(cls, secret, user, realm, encoding=None):
|
||||
# NOTE: this was deliberately written so that raw bytes are passed through
|
||||
# unchanged, the encoding kwd is only used to handle unicode values.
|
||||
if not encoding:
|
||||
encoding = cls.default_encoding
|
||||
uh.validate_secret(secret)
|
||||
if isinstance(secret, unicode):
|
||||
secret = secret.encode(encoding)
|
||||
user = to_bytes(user, encoding, "user")
|
||||
realm = to_bytes(realm, encoding, "realm")
|
||||
data = render_bytes("%s:%s:%s", user, realm, secret)
|
||||
return hashlib.md5(data).hexdigest()
|
||||
|
||||
@classmethod
|
||||
def _norm_hash(cls, hash):
|
||||
"""normalize hash to native string, and validate it"""
|
||||
hash = to_native_str(hash, param="hash")
|
||||
if len(hash) != 32:
|
||||
raise uh.exc.MalformedHashError(cls, "wrong size")
|
||||
for char in hash:
|
||||
if char not in uh.LC_HEX_CHARS:
|
||||
raise uh.exc.MalformedHashError(cls, "invalid chars in hash")
|
||||
return hash
|
||||
|
||||
@classmethod
|
||||
def verify(cls, secret, hash, user, realm, encoding="utf-8"):
|
||||
hash = cls._norm_hash(hash)
|
||||
other = cls.hash(secret, user, realm, encoding)
|
||||
return consteq(hash, other)
|
||||
|
||||
@classmethod
|
||||
def identify(cls, hash):
|
||||
try:
|
||||
cls._norm_hash(hash)
|
||||
except ValueError:
|
||||
return False
|
||||
return True
|
||||
|
||||
@uh.deprecated_method(deprecated="1.7", removed="2.0")
|
||||
@classmethod
|
||||
def genconfig(cls):
|
||||
return cls.hash("", "", "")
|
||||
|
||||
@uh.deprecated_method(deprecated="1.7", removed="2.0")
|
||||
@classmethod
|
||||
def genhash(cls, secret, config, user, realm, encoding=None):
|
||||
# NOTE: 'config' is ignored, as this hash has no salting / other configuration.
|
||||
# just have to make sure it's valid.
|
||||
cls._norm_hash(config)
|
||||
return cls.hash(secret, user, realm, encoding)
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
512
venv/lib/python3.12/site-packages/passlib/handlers/django.py
Normal file
512
venv/lib/python3.12/site-packages/passlib/handlers/django.py
Normal file
@@ -0,0 +1,512 @@
|
||||
"""passlib.handlers.django- Django password hash support"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
# core
|
||||
from base64 import b64encode
|
||||
from binascii import hexlify
|
||||
from hashlib import md5, sha1, sha256
|
||||
import logging; log = logging.getLogger(__name__)
|
||||
# site
|
||||
# pkg
|
||||
from passlib.handlers.bcrypt import _wrapped_bcrypt
|
||||
from passlib.hash import argon2, bcrypt, pbkdf2_sha1, pbkdf2_sha256
|
||||
from passlib.utils import to_unicode, rng, getrandstr
|
||||
from passlib.utils.binary import BASE64_CHARS
|
||||
from passlib.utils.compat import str_to_uascii, uascii_to_str, unicode, u
|
||||
from passlib.crypto.digest import pbkdf2_hmac
|
||||
import passlib.utils.handlers as uh
|
||||
# local
|
||||
__all__ = [
|
||||
"django_salted_sha1",
|
||||
"django_salted_md5",
|
||||
"django_bcrypt",
|
||||
"django_pbkdf2_sha1",
|
||||
"django_pbkdf2_sha256",
|
||||
"django_argon2",
|
||||
"django_des_crypt",
|
||||
"django_disabled",
|
||||
]
|
||||
|
||||
#=============================================================================
|
||||
# lazy imports & constants
|
||||
#=============================================================================
|
||||
|
||||
# imported by django_des_crypt._calc_checksum()
|
||||
des_crypt = None
|
||||
|
||||
def _import_des_crypt():
|
||||
global des_crypt
|
||||
if des_crypt is None:
|
||||
from passlib.hash import des_crypt
|
||||
return des_crypt
|
||||
|
||||
# django 1.4's salt charset
|
||||
SALT_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
||||
|
||||
#=============================================================================
|
||||
# salted hashes
|
||||
#=============================================================================
|
||||
class DjangoSaltedHash(uh.HasSalt, uh.GenericHandler):
|
||||
"""base class providing common code for django hashes"""
|
||||
# name, ident, checksum_size must be set by subclass.
|
||||
# ident must include "$" suffix.
|
||||
setting_kwds = ("salt", "salt_size")
|
||||
|
||||
# NOTE: django 1.0-1.3 would accept empty salt strings.
|
||||
# django 1.4 won't, but this appears to be regression
|
||||
# (https://code.djangoproject.com/ticket/18144)
|
||||
# so presumably it will be fixed in a later release.
|
||||
default_salt_size = 12
|
||||
max_salt_size = None
|
||||
salt_chars = SALT_CHARS
|
||||
|
||||
checksum_chars = uh.LOWER_HEX_CHARS
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, hash):
|
||||
salt, chk = uh.parse_mc2(hash, cls.ident, handler=cls)
|
||||
return cls(salt=salt, checksum=chk)
|
||||
|
||||
def to_string(self):
|
||||
return uh.render_mc2(self.ident, self.salt, self.checksum)
|
||||
|
||||
# NOTE: only used by PBKDF2
|
||||
class DjangoVariableHash(uh.HasRounds, DjangoSaltedHash):
|
||||
"""base class providing common code for django hashes w/ variable rounds"""
|
||||
setting_kwds = DjangoSaltedHash.setting_kwds + ("rounds",)
|
||||
|
||||
min_rounds = 1
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, hash):
|
||||
rounds, salt, chk = uh.parse_mc3(hash, cls.ident, handler=cls)
|
||||
return cls(rounds=rounds, salt=salt, checksum=chk)
|
||||
|
||||
def to_string(self):
|
||||
return uh.render_mc3(self.ident, self.rounds, self.salt, self.checksum)
|
||||
|
||||
class django_salted_sha1(DjangoSaltedHash):
|
||||
"""This class implements Django's Salted SHA1 hash, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports a variable-length salt, and uses a single round of SHA1.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
|
||||
|
||||
:type salt: str
|
||||
:param salt:
|
||||
Optional salt string.
|
||||
If not specified, a 12 character one will be autogenerated (this is recommended).
|
||||
If specified, may be any series of characters drawn from the regexp range ``[0-9a-zA-Z]``.
|
||||
|
||||
:type salt_size: int
|
||||
:param salt_size:
|
||||
Optional number of characters to use when autogenerating new salts.
|
||||
Defaults to 12, but can be any positive value.
|
||||
|
||||
This should be compatible with Django 1.4's :class:`!SHA1PasswordHasher` class.
|
||||
|
||||
.. versionchanged: 1.6
|
||||
This class now generates 12-character salts instead of 5,
|
||||
and generated salts uses the character range ``[0-9a-zA-Z]`` instead of
|
||||
the ``[0-9a-f]``. This is to be compatible with how Django >= 1.4
|
||||
generates these hashes; but hashes generated in this manner will still be
|
||||
correctly interpreted by earlier versions of Django.
|
||||
"""
|
||||
name = "django_salted_sha1"
|
||||
django_name = "sha1"
|
||||
ident = u("sha1$")
|
||||
checksum_size = 40
|
||||
|
||||
def _calc_checksum(self, secret):
|
||||
if isinstance(secret, unicode):
|
||||
secret = secret.encode("utf-8")
|
||||
return str_to_uascii(sha1(self.salt.encode("ascii") + secret).hexdigest())
|
||||
|
||||
class django_salted_md5(DjangoSaltedHash):
|
||||
"""This class implements Django's Salted MD5 hash, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports a variable-length salt, and uses a single round of MD5.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
|
||||
|
||||
:type salt: str
|
||||
:param salt:
|
||||
Optional salt string.
|
||||
If not specified, a 12 character one will be autogenerated (this is recommended).
|
||||
If specified, may be any series of characters drawn from the regexp range ``[0-9a-zA-Z]``.
|
||||
|
||||
:type salt_size: int
|
||||
:param salt_size:
|
||||
Optional number of characters to use when autogenerating new salts.
|
||||
Defaults to 12, but can be any positive value.
|
||||
|
||||
This should be compatible with the hashes generated by
|
||||
Django 1.4's :class:`!MD5PasswordHasher` class.
|
||||
|
||||
.. versionchanged: 1.6
|
||||
This class now generates 12-character salts instead of 5,
|
||||
and generated salts uses the character range ``[0-9a-zA-Z]`` instead of
|
||||
the ``[0-9a-f]``. This is to be compatible with how Django >= 1.4
|
||||
generates these hashes; but hashes generated in this manner will still be
|
||||
correctly interpreted by earlier versions of Django.
|
||||
"""
|
||||
name = "django_salted_md5"
|
||||
django_name = "md5"
|
||||
ident = u("md5$")
|
||||
checksum_size = 32
|
||||
|
||||
def _calc_checksum(self, secret):
|
||||
if isinstance(secret, unicode):
|
||||
secret = secret.encode("utf-8")
|
||||
return str_to_uascii(md5(self.salt.encode("ascii") + secret).hexdigest())
|
||||
|
||||
#=============================================================================
|
||||
# BCrypt
|
||||
#=============================================================================
|
||||
|
||||
django_bcrypt = uh.PrefixWrapper("django_bcrypt", bcrypt,
|
||||
prefix=u('bcrypt$'), ident=u("bcrypt$"),
|
||||
# NOTE: this docstring is duplicated in the docs, since sphinx
|
||||
# seems to be having trouble reading it via autodata::
|
||||
doc="""This class implements Django 1.4's BCrypt wrapper, and follows the :ref:`password-hash-api`.
|
||||
|
||||
This is identical to :class:`!bcrypt` itself, but with
|
||||
the Django-specific prefix ``"bcrypt$"`` prepended.
|
||||
|
||||
See :doc:`/lib/passlib.hash.bcrypt` for more details,
|
||||
the usage and behavior is identical.
|
||||
|
||||
This should be compatible with the hashes generated by
|
||||
Django 1.4's :class:`!BCryptPasswordHasher` class.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
""")
|
||||
django_bcrypt.django_name = "bcrypt"
|
||||
django_bcrypt._using_clone_attrs += ("django_name",)
|
||||
|
||||
#=============================================================================
|
||||
# BCRYPT + SHA256
|
||||
#=============================================================================
|
||||
|
||||
class django_bcrypt_sha256(_wrapped_bcrypt):
|
||||
"""This class implements Django 1.6's Bcrypt+SHA256 hash, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports a variable-length salt, and a variable number of rounds.
|
||||
|
||||
While the algorithm and format is somewhat different,
|
||||
the api and options for this hash are identical to :class:`!bcrypt` itself,
|
||||
see :doc:`bcrypt </lib/passlib.hash.bcrypt>` for more details.
|
||||
|
||||
.. versionadded:: 1.6.2
|
||||
"""
|
||||
name = "django_bcrypt_sha256"
|
||||
django_name = "bcrypt_sha256"
|
||||
_digest = sha256
|
||||
|
||||
# sample hash:
|
||||
# bcrypt_sha256$$2a$06$/3OeRpbOf8/l6nPPRdZPp.nRiyYqPobEZGdNRBWihQhiFDh1ws1tu
|
||||
|
||||
# XXX: we can't use .ident attr due to bcrypt code using it.
|
||||
# working around that via django_prefix
|
||||
django_prefix = u('bcrypt_sha256$')
|
||||
|
||||
@classmethod
|
||||
def identify(cls, hash):
|
||||
hash = uh.to_unicode_for_identify(hash)
|
||||
if not hash:
|
||||
return False
|
||||
return hash.startswith(cls.django_prefix)
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, hash):
|
||||
hash = to_unicode(hash, "ascii", "hash")
|
||||
if not hash.startswith(cls.django_prefix):
|
||||
raise uh.exc.InvalidHashError(cls)
|
||||
bhash = hash[len(cls.django_prefix):]
|
||||
if not bhash.startswith("$2"):
|
||||
raise uh.exc.MalformedHashError(cls)
|
||||
return super(django_bcrypt_sha256, cls).from_string(bhash)
|
||||
|
||||
def to_string(self):
|
||||
bhash = super(django_bcrypt_sha256, self).to_string()
|
||||
return uascii_to_str(self.django_prefix) + bhash
|
||||
|
||||
def _calc_checksum(self, secret):
|
||||
if isinstance(secret, unicode):
|
||||
secret = secret.encode("utf-8")
|
||||
secret = hexlify(self._digest(secret).digest())
|
||||
return super(django_bcrypt_sha256, self)._calc_checksum(secret)
|
||||
|
||||
#=============================================================================
|
||||
# PBKDF2 variants
|
||||
#=============================================================================
|
||||
|
||||
class django_pbkdf2_sha256(DjangoVariableHash):
|
||||
"""This class implements Django's PBKDF2-HMAC-SHA256 hash, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports a variable-length salt, and a variable number of rounds.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
|
||||
|
||||
:type salt: str
|
||||
:param salt:
|
||||
Optional salt string.
|
||||
If not specified, a 12 character one will be autogenerated (this is recommended).
|
||||
If specified, may be any series of characters drawn from the regexp range ``[0-9a-zA-Z]``.
|
||||
|
||||
:type salt_size: int
|
||||
:param salt_size:
|
||||
Optional number of characters to use when autogenerating new salts.
|
||||
Defaults to 12, but can be any positive value.
|
||||
|
||||
:type rounds: int
|
||||
:param rounds:
|
||||
Optional number of rounds to use.
|
||||
Defaults to 29000, but must be within ``range(1,1<<32)``.
|
||||
|
||||
:type relaxed: bool
|
||||
:param relaxed:
|
||||
By default, providing an invalid value for one of the other
|
||||
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
|
||||
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
|
||||
will be issued instead. Correctable errors include ``rounds``
|
||||
that are too small or too large, and ``salt`` strings that are too long.
|
||||
|
||||
This should be compatible with the hashes generated by
|
||||
Django 1.4's :class:`!PBKDF2PasswordHasher` class.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
"""
|
||||
name = "django_pbkdf2_sha256"
|
||||
django_name = "pbkdf2_sha256"
|
||||
ident = u('pbkdf2_sha256$')
|
||||
min_salt_size = 1
|
||||
max_rounds = 0xffffffff # setting at 32-bit limit for now
|
||||
checksum_chars = uh.PADDED_BASE64_CHARS
|
||||
checksum_size = 44 # 32 bytes -> base64
|
||||
default_rounds = pbkdf2_sha256.default_rounds # NOTE: django 1.6 uses 12000
|
||||
_digest = "sha256"
|
||||
|
||||
def _calc_checksum(self, secret):
|
||||
# NOTE: secret & salt will be encoded using UTF-8 by pbkdf2_hmac()
|
||||
hash = pbkdf2_hmac(self._digest, secret, self.salt, self.rounds)
|
||||
return b64encode(hash).rstrip().decode("ascii")
|
||||
|
||||
class django_pbkdf2_sha1(django_pbkdf2_sha256):
|
||||
"""This class implements Django's PBKDF2-HMAC-SHA1 hash, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports a variable-length salt, and a variable number of rounds.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
|
||||
|
||||
:type salt: str
|
||||
:param salt:
|
||||
Optional salt string.
|
||||
If not specified, a 12 character one will be autogenerated (this is recommended).
|
||||
If specified, may be any series of characters drawn from the regexp range ``[0-9a-zA-Z]``.
|
||||
|
||||
:type salt_size: int
|
||||
:param salt_size:
|
||||
Optional number of characters to use when autogenerating new salts.
|
||||
Defaults to 12, but can be any positive value.
|
||||
|
||||
:type rounds: int
|
||||
:param rounds:
|
||||
Optional number of rounds to use.
|
||||
Defaults to 131000, but must be within ``range(1,1<<32)``.
|
||||
|
||||
:type relaxed: bool
|
||||
:param relaxed:
|
||||
By default, providing an invalid value for one of the other
|
||||
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
|
||||
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
|
||||
will be issued instead. Correctable errors include ``rounds``
|
||||
that are too small or too large, and ``salt`` strings that are too long.
|
||||
|
||||
This should be compatible with the hashes generated by
|
||||
Django 1.4's :class:`!PBKDF2SHA1PasswordHasher` class.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
"""
|
||||
name = "django_pbkdf2_sha1"
|
||||
django_name = "pbkdf2_sha1"
|
||||
ident = u('pbkdf2_sha1$')
|
||||
checksum_size = 28 # 20 bytes -> base64
|
||||
default_rounds = pbkdf2_sha1.default_rounds # NOTE: django 1.6 uses 12000
|
||||
_digest = "sha1"
|
||||
|
||||
#=============================================================================
|
||||
# Argon2
|
||||
#=============================================================================
|
||||
|
||||
# NOTE: as of 2019-11-11, Django's Argon2PasswordHasher only supports Type I;
|
||||
# so limiting this to ensure that as well.
|
||||
|
||||
django_argon2 = uh.PrefixWrapper(
|
||||
name="django_argon2",
|
||||
wrapped=argon2.using(type="I"),
|
||||
prefix=u('argon2'),
|
||||
ident=u('argon2$argon2i$'),
|
||||
# NOTE: this docstring is duplicated in the docs, since sphinx
|
||||
# seems to be having trouble reading it via autodata::
|
||||
doc="""This class implements Django 1.10's Argon2 wrapper, and follows the :ref:`password-hash-api`.
|
||||
|
||||
This is identical to :class:`!argon2` itself, but with
|
||||
the Django-specific prefix ``"argon2$"`` prepended.
|
||||
|
||||
See :doc:`argon2 </lib/passlib.hash.argon2>` for more details,
|
||||
the usage and behavior is identical.
|
||||
|
||||
This should be compatible with the hashes generated by
|
||||
Django 1.10's :class:`!Argon2PasswordHasher` class.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
""")
|
||||
django_argon2.django_name = "argon2"
|
||||
django_argon2._using_clone_attrs += ("django_name",)
|
||||
|
||||
#=============================================================================
|
||||
# DES
|
||||
#=============================================================================
|
||||
class django_des_crypt(uh.TruncateMixin, uh.HasSalt, uh.GenericHandler):
|
||||
"""This class implements Django's :class:`des_crypt` wrapper, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports a fixed-length salt.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
|
||||
|
||||
:type salt: str
|
||||
:param salt:
|
||||
Optional salt string.
|
||||
If not specified, one will be autogenerated (this is recommended).
|
||||
If specified, it must be 2 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
|
||||
|
||||
:param bool truncate_error:
|
||||
By default, django_des_crypt will silently truncate passwords larger than 8 bytes.
|
||||
Setting ``truncate_error=True`` will cause :meth:`~passlib.ifc.PasswordHash.hash`
|
||||
to raise a :exc:`~passlib.exc.PasswordTruncateError` instead.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
This should be compatible with the hashes generated by
|
||||
Django 1.4's :class:`!CryptPasswordHasher` class.
|
||||
Note that Django only supports this hash on Unix systems
|
||||
(though :class:`!django_des_crypt` is available cross-platform
|
||||
under Passlib).
|
||||
|
||||
.. versionchanged:: 1.6
|
||||
This class will now accept hashes with empty salt strings,
|
||||
since Django 1.4 generates them this way.
|
||||
"""
|
||||
name = "django_des_crypt"
|
||||
django_name = "crypt"
|
||||
setting_kwds = ("salt", "salt_size", "truncate_error")
|
||||
ident = u("crypt$")
|
||||
checksum_chars = salt_chars = uh.HASH64_CHARS
|
||||
checksum_size = 11
|
||||
min_salt_size = default_salt_size = 2
|
||||
truncate_size = 8
|
||||
|
||||
# NOTE: regarding duplicate salt field:
|
||||
#
|
||||
# django 1.0 had a "crypt$<salt1>$<salt2><digest>" hash format,
|
||||
# used [a-z0-9] to generate a 5 char salt, stored it in salt1,
|
||||
# duplicated the first two chars of salt1 as salt2.
|
||||
# it would throw an error if salt1 was empty.
|
||||
#
|
||||
# django 1.4 started generating 2 char salt using the full alphabet,
|
||||
# left salt1 empty, and only paid attention to salt2.
|
||||
#
|
||||
# in order to be compatible with django 1.0, the hashes generated
|
||||
# by this function will always include salt1, unless the following
|
||||
# class-level field is disabled (mainly used for testing)
|
||||
use_duplicate_salt = True
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, hash):
|
||||
salt, chk = uh.parse_mc2(hash, cls.ident, handler=cls)
|
||||
if chk:
|
||||
# chk should be full des_crypt hash
|
||||
if not salt:
|
||||
# django 1.4 always uses empty salt field,
|
||||
# so extract salt from des_crypt hash <chk>
|
||||
salt = chk[:2]
|
||||
elif salt[:2] != chk[:2]:
|
||||
# django 1.0 stored 5 chars in salt field, and duplicated
|
||||
# the first two chars in <chk>. we keep the full salt,
|
||||
# but make sure the first two chars match as sanity check.
|
||||
raise uh.exc.MalformedHashError(cls,
|
||||
"first two digits of salt and checksum must match")
|
||||
# in all cases, strip salt chars from <chk>
|
||||
chk = chk[2:]
|
||||
return cls(salt=salt, checksum=chk)
|
||||
|
||||
def to_string(self):
|
||||
salt = self.salt
|
||||
chk = salt[:2] + self.checksum
|
||||
if self.use_duplicate_salt:
|
||||
# filling in salt field, so that we're compatible with django 1.0
|
||||
return uh.render_mc2(self.ident, salt, chk)
|
||||
else:
|
||||
# django 1.4+ style hash
|
||||
return uh.render_mc2(self.ident, "", chk)
|
||||
|
||||
def _calc_checksum(self, secret):
|
||||
# NOTE: we lazily import des_crypt,
|
||||
# since most django deploys won't use django_des_crypt
|
||||
global des_crypt
|
||||
if des_crypt is None:
|
||||
_import_des_crypt()
|
||||
# check for truncation (during .hash() calls only)
|
||||
if self.use_defaults:
|
||||
self._check_truncate_policy(secret)
|
||||
return des_crypt(salt=self.salt[:2])._calc_checksum(secret)
|
||||
|
||||
class django_disabled(uh.ifc.DisabledHash, uh.StaticHandler):
|
||||
"""This class provides disabled password behavior for Django, and follows the :ref:`password-hash-api`.
|
||||
|
||||
This class does not implement a hash, but instead
|
||||
claims the special hash string ``"!"`` which Django uses
|
||||
to indicate an account's password has been disabled.
|
||||
|
||||
* newly encrypted passwords will hash to ``"!"``.
|
||||
* it rejects all passwords.
|
||||
|
||||
.. note::
|
||||
|
||||
Django 1.6 prepends a randomly generated 40-char alphanumeric string
|
||||
to each unusuable password. This class recognizes such strings,
|
||||
but for backwards compatibility, still returns ``"!"``.
|
||||
|
||||
See `<https://code.djangoproject.com/ticket/20079>`_ for why
|
||||
Django appends an alphanumeric string.
|
||||
|
||||
.. versionchanged:: 1.6.2 added Django 1.6 support
|
||||
|
||||
.. versionchanged:: 1.7 started appending an alphanumeric string.
|
||||
"""
|
||||
name = "django_disabled"
|
||||
_hash_prefix = u("!")
|
||||
suffix_length = 40
|
||||
|
||||
# XXX: move this to StaticHandler, or wherever _hash_prefix is being used?
|
||||
@classmethod
|
||||
def identify(cls, hash):
|
||||
hash = uh.to_unicode_for_identify(hash)
|
||||
return hash.startswith(cls._hash_prefix)
|
||||
|
||||
def _calc_checksum(self, secret):
|
||||
# generate random suffix to match django's behavior
|
||||
return getrandstr(rng, BASE64_CHARS[:-2], self.suffix_length)
|
||||
|
||||
@classmethod
|
||||
def verify(cls, secret, hash):
|
||||
uh.validate_secret(secret)
|
||||
if not cls.identify(hash):
|
||||
raise uh.exc.InvalidHashError(cls)
|
||||
return False
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
214
venv/lib/python3.12/site-packages/passlib/handlers/fshp.py
Normal file
214
venv/lib/python3.12/site-packages/passlib/handlers/fshp.py
Normal file
@@ -0,0 +1,214 @@
|
||||
"""passlib.handlers.fshp
|
||||
"""
|
||||
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
# core
|
||||
from base64 import b64encode, b64decode
|
||||
import re
|
||||
import logging; log = logging.getLogger(__name__)
|
||||
# site
|
||||
# pkg
|
||||
from passlib.utils import to_unicode
|
||||
import passlib.utils.handlers as uh
|
||||
from passlib.utils.compat import bascii_to_str, iteritems, u,\
|
||||
unicode
|
||||
from passlib.crypto.digest import pbkdf1
|
||||
# local
|
||||
__all__ = [
|
||||
'fshp',
|
||||
]
|
||||
#=============================================================================
|
||||
# sha1-crypt
|
||||
#=============================================================================
|
||||
class fshp(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
|
||||
"""This class implements the FSHP password hash, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports a variable-length salt, and a variable number of rounds.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
|
||||
|
||||
:param salt:
|
||||
Optional raw salt string.
|
||||
If not specified, one will be autogenerated (this is recommended).
|
||||
|
||||
:param salt_size:
|
||||
Optional number of bytes to use when autogenerating new salts.
|
||||
Defaults to 16 bytes, but can be any non-negative value.
|
||||
|
||||
:param rounds:
|
||||
Optional number of rounds to use.
|
||||
Defaults to 480000, must be between 1 and 4294967295, inclusive.
|
||||
|
||||
:param variant:
|
||||
Optionally specifies variant of FSHP to use.
|
||||
|
||||
* ``0`` - uses SHA-1 digest (deprecated).
|
||||
* ``1`` - uses SHA-2/256 digest (default).
|
||||
* ``2`` - uses SHA-2/384 digest.
|
||||
* ``3`` - uses SHA-2/512 digest.
|
||||
|
||||
:type relaxed: bool
|
||||
:param relaxed:
|
||||
By default, providing an invalid value for one of the other
|
||||
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
|
||||
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
|
||||
will be issued instead. Correctable errors include ``rounds``
|
||||
that are too small or too large, and ``salt`` strings that are too long.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
"""
|
||||
|
||||
#===================================================================
|
||||
# class attrs
|
||||
#===================================================================
|
||||
#--GenericHandler--
|
||||
name = "fshp"
|
||||
setting_kwds = ("salt", "salt_size", "rounds", "variant")
|
||||
checksum_chars = uh.PADDED_BASE64_CHARS
|
||||
ident = u("{FSHP")
|
||||
# checksum_size is property() that depends on variant
|
||||
|
||||
#--HasRawSalt--
|
||||
default_salt_size = 16 # current passlib default, FSHP uses 8
|
||||
max_salt_size = None
|
||||
|
||||
#--HasRounds--
|
||||
# FIXME: should probably use different default rounds
|
||||
# based on the variant. setting for default variant (sha256) for now.
|
||||
default_rounds = 480000 # current passlib default, FSHP uses 4096
|
||||
min_rounds = 1 # set by FSHP
|
||||
max_rounds = 4294967295 # 32-bit integer limit - not set by FSHP
|
||||
rounds_cost = "linear"
|
||||
|
||||
#--variants--
|
||||
default_variant = 1
|
||||
_variant_info = {
|
||||
# variant: (hash name, digest size)
|
||||
0: ("sha1", 20),
|
||||
1: ("sha256", 32),
|
||||
2: ("sha384", 48),
|
||||
3: ("sha512", 64),
|
||||
}
|
||||
_variant_aliases = dict(
|
||||
[(unicode(k),k) for k in _variant_info] +
|
||||
[(v[0],k) for k,v in iteritems(_variant_info)]
|
||||
)
|
||||
|
||||
#===================================================================
|
||||
# configuration
|
||||
#===================================================================
|
||||
@classmethod
|
||||
def using(cls, variant=None, **kwds):
|
||||
subcls = super(fshp, cls).using(**kwds)
|
||||
if variant is not None:
|
||||
subcls.default_variant = cls._norm_variant(variant)
|
||||
return subcls
|
||||
|
||||
#===================================================================
|
||||
# instance attrs
|
||||
#===================================================================
|
||||
variant = None
|
||||
|
||||
#===================================================================
|
||||
# init
|
||||
#===================================================================
|
||||
def __init__(self, variant=None, **kwds):
|
||||
# NOTE: variant must be set first, since it controls checksum size, etc.
|
||||
self.use_defaults = kwds.get("use_defaults") # load this early
|
||||
if variant is not None:
|
||||
variant = self._norm_variant(variant)
|
||||
elif self.use_defaults:
|
||||
variant = self.default_variant
|
||||
assert self._norm_variant(variant) == variant, "invalid default variant: %r" % (variant,)
|
||||
else:
|
||||
raise TypeError("no variant specified")
|
||||
self.variant = variant
|
||||
super(fshp, self).__init__(**kwds)
|
||||
|
||||
@classmethod
|
||||
def _norm_variant(cls, variant):
|
||||
if isinstance(variant, bytes):
|
||||
variant = variant.decode("ascii")
|
||||
if isinstance(variant, unicode):
|
||||
try:
|
||||
variant = cls._variant_aliases[variant]
|
||||
except KeyError:
|
||||
raise ValueError("invalid fshp variant")
|
||||
if not isinstance(variant, int):
|
||||
raise TypeError("fshp variant must be int or known alias")
|
||||
if variant not in cls._variant_info:
|
||||
raise ValueError("invalid fshp variant")
|
||||
return variant
|
||||
|
||||
@property
|
||||
def checksum_alg(self):
|
||||
return self._variant_info[self.variant][0]
|
||||
|
||||
@property
|
||||
def checksum_size(self):
|
||||
return self._variant_info[self.variant][1]
|
||||
|
||||
#===================================================================
|
||||
# formatting
|
||||
#===================================================================
|
||||
|
||||
_hash_regex = re.compile(u(r"""
|
||||
^
|
||||
\{FSHP
|
||||
(\d+)\| # variant
|
||||
(\d+)\| # salt size
|
||||
(\d+)\} # rounds
|
||||
([a-zA-Z0-9+/]+={0,3}) # digest
|
||||
$"""), re.X)
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, hash):
|
||||
hash = to_unicode(hash, "ascii", "hash")
|
||||
m = cls._hash_regex.match(hash)
|
||||
if not m:
|
||||
raise uh.exc.InvalidHashError(cls)
|
||||
variant, salt_size, rounds, data = m.group(1,2,3,4)
|
||||
variant = int(variant)
|
||||
salt_size = int(salt_size)
|
||||
rounds = int(rounds)
|
||||
try:
|
||||
data = b64decode(data.encode("ascii"))
|
||||
except TypeError:
|
||||
raise uh.exc.MalformedHashError(cls)
|
||||
salt = data[:salt_size]
|
||||
chk = data[salt_size:]
|
||||
return cls(salt=salt, checksum=chk, rounds=rounds, variant=variant)
|
||||
|
||||
def to_string(self):
|
||||
chk = self.checksum
|
||||
salt = self.salt
|
||||
data = bascii_to_str(b64encode(salt+chk))
|
||||
return "{FSHP%d|%d|%d}%s" % (self.variant, len(salt), self.rounds, data)
|
||||
|
||||
#===================================================================
|
||||
# backend
|
||||
#===================================================================
|
||||
|
||||
def _calc_checksum(self, secret):
|
||||
if isinstance(secret, unicode):
|
||||
secret = secret.encode("utf-8")
|
||||
# NOTE: for some reason, FSHP uses pbkdf1 with password & salt reversed.
|
||||
# this has only a minimal impact on security,
|
||||
# but it is worth noting this deviation.
|
||||
return pbkdf1(
|
||||
digest=self.checksum_alg,
|
||||
secret=self.salt,
|
||||
salt=secret,
|
||||
rounds=self.rounds,
|
||||
keylen=self.checksum_size,
|
||||
)
|
||||
|
||||
#===================================================================
|
||||
# eoc
|
||||
#===================================================================
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
@@ -0,0 +1,359 @@
|
||||
"""passlib.handlers.digests - plain hash digests
|
||||
"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
# core
|
||||
from base64 import b64encode, b64decode
|
||||
from hashlib import md5, sha1, sha256, sha512
|
||||
import logging; log = logging.getLogger(__name__)
|
||||
import re
|
||||
# site
|
||||
# pkg
|
||||
from passlib.handlers.misc import plaintext
|
||||
from passlib.utils import unix_crypt_schemes, to_unicode
|
||||
from passlib.utils.compat import uascii_to_str, unicode, u
|
||||
from passlib.utils.decor import classproperty
|
||||
import passlib.utils.handlers as uh
|
||||
# local
|
||||
__all__ = [
|
||||
"ldap_plaintext",
|
||||
"ldap_md5",
|
||||
"ldap_sha1",
|
||||
"ldap_salted_md5",
|
||||
"ldap_salted_sha1",
|
||||
"ldap_salted_sha256",
|
||||
"ldap_salted_sha512",
|
||||
|
||||
##"get_active_ldap_crypt_schemes",
|
||||
"ldap_des_crypt",
|
||||
"ldap_bsdi_crypt",
|
||||
"ldap_md5_crypt",
|
||||
"ldap_sha1_crypt",
|
||||
"ldap_bcrypt",
|
||||
"ldap_sha256_crypt",
|
||||
"ldap_sha512_crypt",
|
||||
]
|
||||
|
||||
#=============================================================================
|
||||
# ldap helpers
|
||||
#=============================================================================
|
||||
class _Base64DigestHelper(uh.StaticHandler):
|
||||
"""helper for ldap_md5 / ldap_sha1"""
|
||||
# XXX: could combine this with hex digests in digests.py
|
||||
|
||||
ident = None # required - prefix identifier
|
||||
_hash_func = None # required - hash function
|
||||
_hash_regex = None # required - regexp to recognize hash
|
||||
checksum_chars = uh.PADDED_BASE64_CHARS
|
||||
|
||||
@classproperty
|
||||
def _hash_prefix(cls):
|
||||
"""tell StaticHandler to strip ident from checksum"""
|
||||
return cls.ident
|
||||
|
||||
def _calc_checksum(self, secret):
|
||||
if isinstance(secret, unicode):
|
||||
secret = secret.encode("utf-8")
|
||||
chk = self._hash_func(secret).digest()
|
||||
return b64encode(chk).decode("ascii")
|
||||
|
||||
class _SaltedBase64DigestHelper(uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
|
||||
"""helper for ldap_salted_md5 / ldap_salted_sha1"""
|
||||
setting_kwds = ("salt", "salt_size")
|
||||
checksum_chars = uh.PADDED_BASE64_CHARS
|
||||
|
||||
ident = None # required - prefix identifier
|
||||
_hash_func = None # required - hash function
|
||||
_hash_regex = None # required - regexp to recognize hash
|
||||
min_salt_size = max_salt_size = 4
|
||||
|
||||
# NOTE: openldap implementation uses 4 byte salt,
|
||||
# but it's been reported (issue 30) that some servers use larger salts.
|
||||
# the semi-related rfc3112 recommends support for up to 16 byte salts.
|
||||
min_salt_size = 4
|
||||
default_salt_size = 4
|
||||
max_salt_size = 16
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, hash):
|
||||
hash = to_unicode(hash, "ascii", "hash")
|
||||
m = cls._hash_regex.match(hash)
|
||||
if not m:
|
||||
raise uh.exc.InvalidHashError(cls)
|
||||
try:
|
||||
data = b64decode(m.group("tmp").encode("ascii"))
|
||||
except TypeError:
|
||||
raise uh.exc.MalformedHashError(cls)
|
||||
cs = cls.checksum_size
|
||||
assert cs
|
||||
return cls(checksum=data[:cs], salt=data[cs:])
|
||||
|
||||
def to_string(self):
|
||||
data = self.checksum + self.salt
|
||||
hash = self.ident + b64encode(data).decode("ascii")
|
||||
return uascii_to_str(hash)
|
||||
|
||||
def _calc_checksum(self, secret):
|
||||
if isinstance(secret, unicode):
|
||||
secret = secret.encode("utf-8")
|
||||
return self._hash_func(secret + self.salt).digest()
|
||||
|
||||
#=============================================================================
|
||||
# implementations
|
||||
#=============================================================================
|
||||
class ldap_md5(_Base64DigestHelper):
|
||||
"""This class stores passwords using LDAP's plain MD5 format, and follows the :ref:`password-hash-api`.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods have no optional keywords.
|
||||
"""
|
||||
name = "ldap_md5"
|
||||
ident = u("{MD5}")
|
||||
_hash_func = md5
|
||||
_hash_regex = re.compile(u(r"^\{MD5\}(?P<chk>[+/a-zA-Z0-9]{22}==)$"))
|
||||
|
||||
class ldap_sha1(_Base64DigestHelper):
|
||||
"""This class stores passwords using LDAP's plain SHA1 format, and follows the :ref:`password-hash-api`.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods have no optional keywords.
|
||||
"""
|
||||
name = "ldap_sha1"
|
||||
ident = u("{SHA}")
|
||||
_hash_func = sha1
|
||||
_hash_regex = re.compile(u(r"^\{SHA\}(?P<chk>[+/a-zA-Z0-9]{27}=)$"))
|
||||
|
||||
class ldap_salted_md5(_SaltedBase64DigestHelper):
|
||||
"""This class stores passwords using LDAP's salted MD5 format, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports a 4-16 byte salt.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
|
||||
|
||||
:type salt: bytes
|
||||
:param salt:
|
||||
Optional salt string.
|
||||
If not specified, one will be autogenerated (this is recommended).
|
||||
If specified, it may be any 4-16 byte string.
|
||||
|
||||
:type salt_size: int
|
||||
:param salt_size:
|
||||
Optional number of bytes to use when autogenerating new salts.
|
||||
Defaults to 4 bytes for compatibility with the LDAP spec,
|
||||
but some systems use larger salts, and Passlib supports
|
||||
any value between 4-16.
|
||||
|
||||
:type relaxed: bool
|
||||
:param relaxed:
|
||||
By default, providing an invalid value for one of the other
|
||||
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
|
||||
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
|
||||
will be issued instead. Correctable errors include
|
||||
``salt`` strings that are too long.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
|
||||
.. versionchanged:: 1.6
|
||||
This format now supports variable length salts, instead of a fix 4 bytes.
|
||||
"""
|
||||
name = "ldap_salted_md5"
|
||||
ident = u("{SMD5}")
|
||||
checksum_size = 16
|
||||
_hash_func = md5
|
||||
_hash_regex = re.compile(u(r"^\{SMD5\}(?P<tmp>[+/a-zA-Z0-9]{27,}={0,2})$"))
|
||||
|
||||
class ldap_salted_sha1(_SaltedBase64DigestHelper):
|
||||
"""
|
||||
This class stores passwords using LDAP's "Salted SHA1" format,
|
||||
and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports a 4-16 byte salt.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
|
||||
|
||||
:type salt: bytes
|
||||
:param salt:
|
||||
Optional salt string.
|
||||
If not specified, one will be autogenerated (this is recommended).
|
||||
If specified, it may be any 4-16 byte string.
|
||||
|
||||
:type salt_size: int
|
||||
:param salt_size:
|
||||
Optional number of bytes to use when autogenerating new salts.
|
||||
Defaults to 4 bytes for compatibility with the LDAP spec,
|
||||
but some systems use larger salts, and Passlib supports
|
||||
any value between 4-16.
|
||||
|
||||
:type relaxed: bool
|
||||
:param relaxed:
|
||||
By default, providing an invalid value for one of the other
|
||||
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
|
||||
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
|
||||
will be issued instead. Correctable errors include
|
||||
``salt`` strings that are too long.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
|
||||
.. versionchanged:: 1.6
|
||||
This format now supports variable length salts, instead of a fix 4 bytes.
|
||||
"""
|
||||
name = "ldap_salted_sha1"
|
||||
ident = u("{SSHA}")
|
||||
checksum_size = 20
|
||||
_hash_func = sha1
|
||||
# NOTE: 32 = ceil((20 + 4) * 4/3)
|
||||
_hash_regex = re.compile(u(r"^\{SSHA\}(?P<tmp>[+/a-zA-Z0-9]{32,}={0,2})$"))
|
||||
|
||||
|
||||
|
||||
class ldap_salted_sha256(_SaltedBase64DigestHelper):
|
||||
"""
|
||||
This class stores passwords using LDAP's "Salted SHA2-256" format,
|
||||
and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports a 4-16 byte salt.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
|
||||
|
||||
:type salt: bytes
|
||||
:param salt:
|
||||
Optional salt string.
|
||||
If not specified, one will be autogenerated (this is recommended).
|
||||
If specified, it may be any 4-16 byte string.
|
||||
|
||||
:type salt_size: int
|
||||
:param salt_size:
|
||||
Optional number of bytes to use when autogenerating new salts.
|
||||
Defaults to 8 bytes for compatibility with the LDAP spec,
|
||||
but Passlib supports any value between 4-16.
|
||||
|
||||
:type relaxed: bool
|
||||
:param relaxed:
|
||||
By default, providing an invalid value for one of the other
|
||||
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
|
||||
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
|
||||
will be issued instead. Correctable errors include
|
||||
``salt`` strings that are too long.
|
||||
|
||||
.. versionadded:: 1.7.3
|
||||
"""
|
||||
name = "ldap_salted_sha256"
|
||||
ident = u("{SSHA256}")
|
||||
checksum_size = 32
|
||||
default_salt_size = 8
|
||||
_hash_func = sha256
|
||||
# NOTE: 48 = ceil((32 + 4) * 4/3)
|
||||
_hash_regex = re.compile(u(r"^\{SSHA256\}(?P<tmp>[+/a-zA-Z0-9]{48,}={0,2})$"))
|
||||
|
||||
|
||||
class ldap_salted_sha512(_SaltedBase64DigestHelper):
|
||||
"""
|
||||
This class stores passwords using LDAP's "Salted SHA2-512" format,
|
||||
and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports a 4-16 byte salt.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
|
||||
|
||||
:type salt: bytes
|
||||
:param salt:
|
||||
Optional salt string.
|
||||
If not specified, one will be autogenerated (this is recommended).
|
||||
If specified, it may be any 4-16 byte string.
|
||||
|
||||
:type salt_size: int
|
||||
:param salt_size:
|
||||
Optional number of bytes to use when autogenerating new salts.
|
||||
Defaults to 8 bytes for compatibility with the LDAP spec,
|
||||
but Passlib supports any value between 4-16.
|
||||
|
||||
:type relaxed: bool
|
||||
:param relaxed:
|
||||
By default, providing an invalid value for one of the other
|
||||
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
|
||||
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
|
||||
will be issued instead. Correctable errors include
|
||||
``salt`` strings that are too long.
|
||||
|
||||
.. versionadded:: 1.7.3
|
||||
"""
|
||||
name = "ldap_salted_sha512"
|
||||
ident = u("{SSHA512}")
|
||||
checksum_size = 64
|
||||
default_salt_size = 8
|
||||
_hash_func = sha512
|
||||
# NOTE: 91 = ceil((64 + 4) * 4/3)
|
||||
_hash_regex = re.compile(u(r"^\{SSHA512\}(?P<tmp>[+/a-zA-Z0-9]{91,}={0,2})$"))
|
||||
|
||||
|
||||
class ldap_plaintext(plaintext):
|
||||
"""This class stores passwords in plaintext, and follows the :ref:`password-hash-api`.
|
||||
|
||||
This class acts much like the generic :class:`!passlib.hash.plaintext` handler,
|
||||
except that it will identify a hash only if it does NOT begin with the ``{XXX}`` identifier prefix
|
||||
used by RFC2307 passwords.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.hash`, :meth:`~passlib.ifc.PasswordHash.genhash`, and :meth:`~passlib.ifc.PasswordHash.verify` methods all require the
|
||||
following additional contextual keyword:
|
||||
|
||||
:type encoding: str
|
||||
:param encoding:
|
||||
This controls the character encoding to use (defaults to ``utf-8``).
|
||||
|
||||
This encoding will be used to encode :class:`!unicode` passwords
|
||||
under Python 2, and decode :class:`!bytes` hashes under Python 3.
|
||||
|
||||
.. versionchanged:: 1.6
|
||||
The ``encoding`` keyword was added.
|
||||
"""
|
||||
# NOTE: this subclasses plaintext, since all it does differently
|
||||
# is override identify()
|
||||
|
||||
name = "ldap_plaintext"
|
||||
_2307_pat = re.compile(u(r"^\{\w+\}.*$"))
|
||||
|
||||
@uh.deprecated_method(deprecated="1.7", removed="2.0")
|
||||
@classmethod
|
||||
def genconfig(cls):
|
||||
# Overridding plaintext.genconfig() since it returns "",
|
||||
# but have to return non-empty value due to identify() below
|
||||
return "!"
|
||||
|
||||
@classmethod
|
||||
def identify(cls, hash):
|
||||
# NOTE: identifies all strings EXCEPT those with {XXX} prefix
|
||||
hash = uh.to_unicode_for_identify(hash)
|
||||
return bool(hash) and cls._2307_pat.match(hash) is None
|
||||
|
||||
#=============================================================================
|
||||
# {CRYPT} wrappers
|
||||
# the following are wrappers around the base crypt algorithms,
|
||||
# which add the ldap required {CRYPT} prefix
|
||||
#=============================================================================
|
||||
ldap_crypt_schemes = [ 'ldap_' + name for name in unix_crypt_schemes ]
|
||||
|
||||
def _init_ldap_crypt_handlers():
|
||||
# NOTE: I don't like to implicitly modify globals() like this,
|
||||
# but don't want to write out all these handlers out either :)
|
||||
g = globals()
|
||||
for wname in unix_crypt_schemes:
|
||||
name = 'ldap_' + wname
|
||||
g[name] = uh.PrefixWrapper(name, wname, prefix=u("{CRYPT}"), lazy=True)
|
||||
del g
|
||||
_init_ldap_crypt_handlers()
|
||||
|
||||
##_lcn_host = None
|
||||
##def get_host_ldap_crypt_schemes():
|
||||
## global _lcn_host
|
||||
## if _lcn_host is None:
|
||||
## from passlib.hosts import host_context
|
||||
## schemes = host_context.schemes()
|
||||
## _lcn_host = [
|
||||
## "ldap_" + name
|
||||
## for name in unix_crypt_names
|
||||
## if name in schemes
|
||||
## ]
|
||||
## return _lcn_host
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
346
venv/lib/python3.12/site-packages/passlib/handlers/md5_crypt.py
Normal file
346
venv/lib/python3.12/site-packages/passlib/handlers/md5_crypt.py
Normal file
@@ -0,0 +1,346 @@
|
||||
"""passlib.handlers.md5_crypt - md5-crypt algorithm"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
# core
|
||||
from hashlib import md5
|
||||
import logging; log = logging.getLogger(__name__)
|
||||
# site
|
||||
# pkg
|
||||
from passlib.utils import safe_crypt, test_crypt, repeat_string
|
||||
from passlib.utils.binary import h64
|
||||
from passlib.utils.compat import unicode, u
|
||||
import passlib.utils.handlers as uh
|
||||
# local
|
||||
__all__ = [
|
||||
"md5_crypt",
|
||||
"apr_md5_crypt",
|
||||
]
|
||||
|
||||
#=============================================================================
|
||||
# pure-python backend
|
||||
#=============================================================================
|
||||
_BNULL = b"\x00"
|
||||
_MD5_MAGIC = b"$1$"
|
||||
_APR_MAGIC = b"$apr1$"
|
||||
|
||||
# pre-calculated offsets used to speed up C digest stage (see notes below).
|
||||
# sequence generated using the following:
|
||||
##perms_order = "p,pp,ps,psp,sp,spp".split(",")
|
||||
##def offset(i):
|
||||
## key = (("p" if i % 2 else "") + ("s" if i % 3 else "") +
|
||||
## ("p" if i % 7 else "") + ("" if i % 2 else "p"))
|
||||
## return perms_order.index(key)
|
||||
##_c_digest_offsets = [(offset(i), offset(i+1)) for i in range(0,42,2)]
|
||||
_c_digest_offsets = (
|
||||
(0, 3), (5, 1), (5, 3), (1, 2), (5, 1), (5, 3), (1, 3),
|
||||
(4, 1), (5, 3), (1, 3), (5, 0), (5, 3), (1, 3), (5, 1),
|
||||
(4, 3), (1, 3), (5, 1), (5, 2), (1, 3), (5, 1), (5, 3),
|
||||
)
|
||||
|
||||
# map used to transpose bytes when encoding final digest
|
||||
_transpose_map = (12, 6, 0, 13, 7, 1, 14, 8, 2, 15, 9, 3, 5, 10, 4, 11)
|
||||
|
||||
def _raw_md5_crypt(pwd, salt, use_apr=False):
|
||||
"""perform raw md5-crypt calculation
|
||||
|
||||
this function provides a pure-python implementation of the internals
|
||||
for the MD5-Crypt algorithms; it doesn't handle any of the
|
||||
parsing/validation of the hash strings themselves.
|
||||
|
||||
:arg pwd: password chars/bytes to hash
|
||||
:arg salt: salt chars to use
|
||||
:arg use_apr: use apache variant
|
||||
|
||||
:returns:
|
||||
encoded checksum chars
|
||||
"""
|
||||
# NOTE: regarding 'apr' format:
|
||||
# really, apache? you had to invent a whole new "$apr1$" format,
|
||||
# when all you did was change the ident incorporated into the hash?
|
||||
# would love to find webpage explaining why just using a portable
|
||||
# implementation of $1$ wasn't sufficient. *nothing else* was changed.
|
||||
|
||||
#===================================================================
|
||||
# init & validate inputs
|
||||
#===================================================================
|
||||
|
||||
# validate secret
|
||||
# XXX: not sure what official unicode policy is, using this as default
|
||||
if isinstance(pwd, unicode):
|
||||
pwd = pwd.encode("utf-8")
|
||||
assert isinstance(pwd, bytes), "pwd not unicode or bytes"
|
||||
if _BNULL in pwd:
|
||||
raise uh.exc.NullPasswordError(md5_crypt)
|
||||
pwd_len = len(pwd)
|
||||
|
||||
# validate salt - should have been taken care of by caller
|
||||
assert isinstance(salt, unicode), "salt not unicode"
|
||||
salt = salt.encode("ascii")
|
||||
assert len(salt) < 9, "salt too large"
|
||||
# NOTE: spec says salts larger than 8 bytes should be truncated,
|
||||
# instead of causing an error. this function assumes that's been
|
||||
# taken care of by the handler class.
|
||||
|
||||
# load APR specific constants
|
||||
if use_apr:
|
||||
magic = _APR_MAGIC
|
||||
else:
|
||||
magic = _MD5_MAGIC
|
||||
|
||||
#===================================================================
|
||||
# digest B - used as subinput to digest A
|
||||
#===================================================================
|
||||
db = md5(pwd + salt + pwd).digest()
|
||||
|
||||
#===================================================================
|
||||
# digest A - used to initialize first round of digest C
|
||||
#===================================================================
|
||||
# start out with pwd + magic + salt
|
||||
a_ctx = md5(pwd + magic + salt)
|
||||
a_ctx_update = a_ctx.update
|
||||
|
||||
# add pwd_len bytes of b, repeating b as many times as needed.
|
||||
a_ctx_update(repeat_string(db, pwd_len))
|
||||
|
||||
# add null chars & first char of password
|
||||
# NOTE: this may have historically been a bug,
|
||||
# where they meant to use db[0] instead of B_NULL,
|
||||
# but the original code memclear'ed db,
|
||||
# and now all implementations have to use this.
|
||||
i = pwd_len
|
||||
evenchar = pwd[:1]
|
||||
while i:
|
||||
a_ctx_update(_BNULL if i & 1 else evenchar)
|
||||
i >>= 1
|
||||
|
||||
# finish A
|
||||
da = a_ctx.digest()
|
||||
|
||||
#===================================================================
|
||||
# digest C - for a 1000 rounds, combine A, S, and P
|
||||
# digests in various ways; in order to burn CPU time.
|
||||
#===================================================================
|
||||
|
||||
# NOTE: the original MD5-Crypt implementation performs the C digest
|
||||
# calculation using the following loop:
|
||||
#
|
||||
##dc = da
|
||||
##i = 0
|
||||
##while i < rounds:
|
||||
## tmp_ctx = md5(pwd if i & 1 else dc)
|
||||
## if i % 3:
|
||||
## tmp_ctx.update(salt)
|
||||
## if i % 7:
|
||||
## tmp_ctx.update(pwd)
|
||||
## tmp_ctx.update(dc if i & 1 else pwd)
|
||||
## dc = tmp_ctx.digest()
|
||||
## i += 1
|
||||
#
|
||||
# The code Passlib uses (below) implements an equivalent algorithm,
|
||||
# it's just been heavily optimized to pre-calculate a large number
|
||||
# of things beforehand. It works off of a couple of observations
|
||||
# about the original algorithm:
|
||||
#
|
||||
# 1. each round is a combination of 'dc', 'salt', and 'pwd'; and the exact
|
||||
# combination is determined by whether 'i' a multiple of 2,3, and/or 7.
|
||||
# 2. since lcm(2,3,7)==42, the series of combinations will repeat
|
||||
# every 42 rounds.
|
||||
# 3. even rounds 0-40 consist of 'hash(dc + round-specific-constant)';
|
||||
# while odd rounds 1-41 consist of hash(round-specific-constant + dc)
|
||||
#
|
||||
# Using these observations, the following code...
|
||||
# * calculates the round-specific combination of salt & pwd for each round 0-41
|
||||
# * runs through as many 42-round blocks as possible (23)
|
||||
# * runs through as many pairs of rounds as needed for remaining rounds (17)
|
||||
# * this results in the required 42*23+2*17=1000 rounds required by md5_crypt.
|
||||
#
|
||||
# this cuts out a lot of the control overhead incurred when running the
|
||||
# original loop 1000 times in python, resulting in ~20% increase in
|
||||
# speed under CPython (though still 2x slower than glibc crypt)
|
||||
|
||||
# prepare the 6 combinations of pwd & salt which are needed
|
||||
# (order of 'perms' must match how _c_digest_offsets was generated)
|
||||
pwd_pwd = pwd+pwd
|
||||
pwd_salt = pwd+salt
|
||||
perms = [pwd, pwd_pwd, pwd_salt, pwd_salt+pwd, salt+pwd, salt+pwd_pwd]
|
||||
|
||||
# build up list of even-round & odd-round constants,
|
||||
# and store in 21-element list as (even,odd) pairs.
|
||||
data = [ (perms[even], perms[odd]) for even, odd in _c_digest_offsets]
|
||||
|
||||
# perform 23 blocks of 42 rounds each (for a total of 966 rounds)
|
||||
dc = da
|
||||
blocks = 23
|
||||
while blocks:
|
||||
for even, odd in data:
|
||||
dc = md5(odd + md5(dc + even).digest()).digest()
|
||||
blocks -= 1
|
||||
|
||||
# perform 17 more pairs of rounds (34 more rounds, for a total of 1000)
|
||||
for even, odd in data[:17]:
|
||||
dc = md5(odd + md5(dc + even).digest()).digest()
|
||||
|
||||
#===================================================================
|
||||
# encode digest using appropriate transpose map
|
||||
#===================================================================
|
||||
return h64.encode_transposed_bytes(dc, _transpose_map).decode("ascii")
|
||||
|
||||
#=============================================================================
|
||||
# handler
|
||||
#=============================================================================
|
||||
class _MD5_Common(uh.HasSalt, uh.GenericHandler):
|
||||
"""common code for md5_crypt and apr_md5_crypt"""
|
||||
#===================================================================
|
||||
# class attrs
|
||||
#===================================================================
|
||||
# name - set in subclass
|
||||
setting_kwds = ("salt", "salt_size")
|
||||
# ident - set in subclass
|
||||
checksum_size = 22
|
||||
checksum_chars = uh.HASH64_CHARS
|
||||
|
||||
max_salt_size = 8
|
||||
salt_chars = uh.HASH64_CHARS
|
||||
|
||||
#===================================================================
|
||||
# methods
|
||||
#===================================================================
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, hash):
|
||||
salt, chk = uh.parse_mc2(hash, cls.ident, handler=cls)
|
||||
return cls(salt=salt, checksum=chk)
|
||||
|
||||
def to_string(self):
|
||||
return uh.render_mc2(self.ident, self.salt, self.checksum)
|
||||
|
||||
# _calc_checksum() - provided by subclass
|
||||
|
||||
#===================================================================
|
||||
# eoc
|
||||
#===================================================================
|
||||
|
||||
class md5_crypt(uh.HasManyBackends, _MD5_Common):
|
||||
"""This class implements the MD5-Crypt password hash, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports a variable-length salt.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
|
||||
|
||||
:type salt: str
|
||||
:param salt:
|
||||
Optional salt string.
|
||||
If not specified, one will be autogenerated (this is recommended).
|
||||
If specified, it must be 0-8 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
|
||||
|
||||
:type salt_size: int
|
||||
:param salt_size:
|
||||
Optional number of characters to use when autogenerating new salts.
|
||||
Defaults to 8, but can be any value between 0 and 8.
|
||||
(This is mainly needed when generating Cisco-compatible hashes,
|
||||
which require ``salt_size=4``).
|
||||
|
||||
:type relaxed: bool
|
||||
:param relaxed:
|
||||
By default, providing an invalid value for one of the other
|
||||
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
|
||||
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
|
||||
will be issued instead. Correctable errors include
|
||||
``salt`` strings that are too long.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
"""
|
||||
#===================================================================
|
||||
# class attrs
|
||||
#===================================================================
|
||||
name = "md5_crypt"
|
||||
ident = u("$1$")
|
||||
|
||||
#===================================================================
|
||||
# methods
|
||||
#===================================================================
|
||||
# FIXME: can't find definitive policy on how md5-crypt handles non-ascii.
|
||||
# all backends currently coerce -> utf-8
|
||||
|
||||
backends = ("os_crypt", "builtin")
|
||||
|
||||
#---------------------------------------------------------------
|
||||
# os_crypt backend
|
||||
#---------------------------------------------------------------
|
||||
@classmethod
|
||||
def _load_backend_os_crypt(cls):
|
||||
if test_crypt("test", '$1$test$pi/xDtU5WFVRqYS6BMU8X/'):
|
||||
cls._set_calc_checksum_backend(cls._calc_checksum_os_crypt)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def _calc_checksum_os_crypt(self, secret):
|
||||
config = self.ident + self.salt
|
||||
hash = safe_crypt(secret, config)
|
||||
if hash is None:
|
||||
# py3's crypt.crypt() can't handle non-utf8 bytes.
|
||||
# fallback to builtin alg, which is always available.
|
||||
return self._calc_checksum_builtin(secret)
|
||||
if not hash.startswith(config) or len(hash) != len(config) + 23:
|
||||
raise uh.exc.CryptBackendError(self, config, hash)
|
||||
return hash[-22:]
|
||||
|
||||
#---------------------------------------------------------------
|
||||
# builtin backend
|
||||
#---------------------------------------------------------------
|
||||
@classmethod
|
||||
def _load_backend_builtin(cls):
|
||||
cls._set_calc_checksum_backend(cls._calc_checksum_builtin)
|
||||
return True
|
||||
|
||||
def _calc_checksum_builtin(self, secret):
|
||||
return _raw_md5_crypt(secret, self.salt)
|
||||
|
||||
#===================================================================
|
||||
# eoc
|
||||
#===================================================================
|
||||
|
||||
class apr_md5_crypt(_MD5_Common):
|
||||
"""This class implements the Apr-MD5-Crypt password hash, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports a variable-length salt.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
|
||||
|
||||
:type salt: str
|
||||
:param salt:
|
||||
Optional salt string.
|
||||
If not specified, one will be autogenerated (this is recommended).
|
||||
If specified, it must be 0-8 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
|
||||
|
||||
:type relaxed: bool
|
||||
:param relaxed:
|
||||
By default, providing an invalid value for one of the other
|
||||
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
|
||||
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
|
||||
will be issued instead. Correctable errors include
|
||||
``salt`` strings that are too long.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
"""
|
||||
#===================================================================
|
||||
# class attrs
|
||||
#===================================================================
|
||||
name = "apr_md5_crypt"
|
||||
ident = u("$apr1$")
|
||||
|
||||
#===================================================================
|
||||
# methods
|
||||
#===================================================================
|
||||
def _calc_checksum(self, secret):
|
||||
return _raw_md5_crypt(secret, self.salt, use_apr=True)
|
||||
|
||||
#===================================================================
|
||||
# eoc
|
||||
#===================================================================
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
269
venv/lib/python3.12/site-packages/passlib/handlers/misc.py
Normal file
269
venv/lib/python3.12/site-packages/passlib/handlers/misc.py
Normal file
@@ -0,0 +1,269 @@
|
||||
"""passlib.handlers.misc - misc generic handlers
|
||||
"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
# core
|
||||
import sys
|
||||
import logging; log = logging.getLogger(__name__)
|
||||
from warnings import warn
|
||||
# site
|
||||
# pkg
|
||||
from passlib.utils import to_native_str, str_consteq
|
||||
from passlib.utils.compat import unicode, u, unicode_or_bytes_types
|
||||
import passlib.utils.handlers as uh
|
||||
# local
|
||||
__all__ = [
|
||||
"unix_disabled",
|
||||
"unix_fallback",
|
||||
"plaintext",
|
||||
]
|
||||
|
||||
#=============================================================================
|
||||
# handler
|
||||
#=============================================================================
|
||||
class unix_fallback(uh.ifc.DisabledHash, uh.StaticHandler):
|
||||
"""This class provides the fallback behavior for unix shadow files, and follows the :ref:`password-hash-api`.
|
||||
|
||||
This class does not implement a hash, but instead provides fallback
|
||||
behavior as found in /etc/shadow on most unix variants.
|
||||
If used, should be the last scheme in the context.
|
||||
|
||||
* this class will positively identify all hash strings.
|
||||
* for security, passwords will always hash to ``!``.
|
||||
* it rejects all passwords if the hash is NOT an empty string (``!`` or ``*`` are frequently used).
|
||||
* by default it rejects all passwords if the hash is an empty string,
|
||||
but if ``enable_wildcard=True`` is passed to verify(),
|
||||
all passwords will be allowed through if the hash is an empty string.
|
||||
|
||||
.. deprecated:: 1.6
|
||||
This has been deprecated due to its "wildcard" feature,
|
||||
and will be removed in Passlib 1.8. Use :class:`unix_disabled` instead.
|
||||
"""
|
||||
name = "unix_fallback"
|
||||
context_kwds = ("enable_wildcard",)
|
||||
|
||||
@classmethod
|
||||
def identify(cls, hash):
|
||||
if isinstance(hash, unicode_or_bytes_types):
|
||||
return True
|
||||
else:
|
||||
raise uh.exc.ExpectedStringError(hash, "hash")
|
||||
|
||||
def __init__(self, enable_wildcard=False, **kwds):
|
||||
warn("'unix_fallback' is deprecated, "
|
||||
"and will be removed in Passlib 1.8; "
|
||||
"please use 'unix_disabled' instead.",
|
||||
DeprecationWarning)
|
||||
super(unix_fallback, self).__init__(**kwds)
|
||||
self.enable_wildcard = enable_wildcard
|
||||
|
||||
def _calc_checksum(self, secret):
|
||||
if self.checksum:
|
||||
# NOTE: hash will generally be "!", but we want to preserve
|
||||
# it in case it's something else, like "*".
|
||||
return self.checksum
|
||||
else:
|
||||
return u("!")
|
||||
|
||||
@classmethod
|
||||
def verify(cls, secret, hash, enable_wildcard=False):
|
||||
uh.validate_secret(secret)
|
||||
if not isinstance(hash, unicode_or_bytes_types):
|
||||
raise uh.exc.ExpectedStringError(hash, "hash")
|
||||
elif hash:
|
||||
return False
|
||||
else:
|
||||
return enable_wildcard
|
||||
|
||||
_MARKER_CHARS = u("*!")
|
||||
_MARKER_BYTES = b"*!"
|
||||
|
||||
class unix_disabled(uh.ifc.DisabledHash, uh.MinimalHandler):
|
||||
"""This class provides disabled password behavior for unix shadow files,
|
||||
and follows the :ref:`password-hash-api`.
|
||||
|
||||
This class does not implement a hash, but instead matches the "disabled account"
|
||||
strings found in ``/etc/shadow`` on most Unix variants. "encrypting" a password
|
||||
will simply return the disabled account marker. It will reject all passwords,
|
||||
no matter the hash string. The :meth:`~passlib.ifc.PasswordHash.hash`
|
||||
method supports one optional keyword:
|
||||
|
||||
:type marker: str
|
||||
:param marker:
|
||||
Optional marker string which overrides the platform default
|
||||
used to indicate a disabled account.
|
||||
|
||||
If not specified, this will default to ``"*"`` on BSD systems,
|
||||
and use the Linux default ``"!"`` for all other platforms.
|
||||
(:attr:`!unix_disabled.default_marker` will contain the default value)
|
||||
|
||||
.. versionadded:: 1.6
|
||||
This class was added as a replacement for the now-deprecated
|
||||
:class:`unix_fallback` class, which had some undesirable features.
|
||||
"""
|
||||
name = "unix_disabled"
|
||||
setting_kwds = ("marker",)
|
||||
context_kwds = ()
|
||||
|
||||
_disable_prefixes = tuple(str(_MARKER_CHARS))
|
||||
|
||||
# TODO: rename attr to 'marker'...
|
||||
if 'bsd' in sys.platform: # pragma: no cover -- runtime detection
|
||||
default_marker = u("*")
|
||||
else:
|
||||
# use the linux default for other systems
|
||||
# (glibc also supports adding old hash after the marker
|
||||
# so it can be restored later).
|
||||
default_marker = u("!")
|
||||
|
||||
@classmethod
|
||||
def using(cls, marker=None, **kwds):
|
||||
subcls = super(unix_disabled, cls).using(**kwds)
|
||||
if marker is not None:
|
||||
if not cls.identify(marker):
|
||||
raise ValueError("invalid marker: %r" % marker)
|
||||
subcls.default_marker = marker
|
||||
return subcls
|
||||
|
||||
@classmethod
|
||||
def identify(cls, hash):
|
||||
# NOTE: technically, anything in the /etc/shadow password field
|
||||
# which isn't valid crypt() output counts as "disabled".
|
||||
# but that's rather ambiguous, and it's hard to predict what
|
||||
# valid output is for unknown crypt() implementations.
|
||||
# so to be on the safe side, we only match things *known*
|
||||
# to be disabled field indicators, and will add others
|
||||
# as they are found. things beginning w/ "$" should *never* match.
|
||||
#
|
||||
# things currently matched:
|
||||
# * linux uses "!"
|
||||
# * bsd uses "*"
|
||||
# * linux may use "!" + hash to disable but preserve original hash
|
||||
# * linux counts empty string as "any password";
|
||||
# this code recognizes it, but treats it the same as "!"
|
||||
if isinstance(hash, unicode):
|
||||
start = _MARKER_CHARS
|
||||
elif isinstance(hash, bytes):
|
||||
start = _MARKER_BYTES
|
||||
else:
|
||||
raise uh.exc.ExpectedStringError(hash, "hash")
|
||||
return not hash or hash[0] in start
|
||||
|
||||
@classmethod
|
||||
def verify(cls, secret, hash):
|
||||
uh.validate_secret(secret)
|
||||
if not cls.identify(hash): # handles typecheck
|
||||
raise uh.exc.InvalidHashError(cls)
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def hash(cls, secret, **kwds):
|
||||
if kwds:
|
||||
uh.warn_hash_settings_deprecation(cls, kwds)
|
||||
return cls.using(**kwds).hash(secret)
|
||||
uh.validate_secret(secret)
|
||||
marker = cls.default_marker
|
||||
assert marker and cls.identify(marker)
|
||||
return to_native_str(marker, param="marker")
|
||||
|
||||
@uh.deprecated_method(deprecated="1.7", removed="2.0")
|
||||
@classmethod
|
||||
def genhash(cls, secret, config, marker=None):
|
||||
if not cls.identify(config):
|
||||
raise uh.exc.InvalidHashError(cls)
|
||||
elif config:
|
||||
# preserve the existing str,since it might contain a disabled password hash ("!" + hash)
|
||||
uh.validate_secret(secret)
|
||||
return to_native_str(config, param="config")
|
||||
else:
|
||||
if marker is not None:
|
||||
cls = cls.using(marker=marker)
|
||||
return cls.hash(secret)
|
||||
|
||||
@classmethod
|
||||
def disable(cls, hash=None):
|
||||
out = cls.hash("")
|
||||
if hash is not None:
|
||||
hash = to_native_str(hash, param="hash")
|
||||
if cls.identify(hash):
|
||||
# extract original hash, so that we normalize marker
|
||||
hash = cls.enable(hash)
|
||||
if hash:
|
||||
out += hash
|
||||
return out
|
||||
|
||||
@classmethod
|
||||
def enable(cls, hash):
|
||||
hash = to_native_str(hash, param="hash")
|
||||
for prefix in cls._disable_prefixes:
|
||||
if hash.startswith(prefix):
|
||||
orig = hash[len(prefix):]
|
||||
if orig:
|
||||
return orig
|
||||
else:
|
||||
raise ValueError("cannot restore original hash")
|
||||
raise uh.exc.InvalidHashError(cls)
|
||||
|
||||
class plaintext(uh.MinimalHandler):
|
||||
"""This class stores passwords in plaintext, and follows the :ref:`password-hash-api`.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.hash`, :meth:`~passlib.ifc.PasswordHash.genhash`, and :meth:`~passlib.ifc.PasswordHash.verify` methods all require the
|
||||
following additional contextual keyword:
|
||||
|
||||
:type encoding: str
|
||||
:param encoding:
|
||||
This controls the character encoding to use (defaults to ``utf-8``).
|
||||
|
||||
This encoding will be used to encode :class:`!unicode` passwords
|
||||
under Python 2, and decode :class:`!bytes` hashes under Python 3.
|
||||
|
||||
.. versionchanged:: 1.6
|
||||
The ``encoding`` keyword was added.
|
||||
"""
|
||||
# NOTE: this is subclassed by ldap_plaintext
|
||||
|
||||
name = "plaintext"
|
||||
setting_kwds = ()
|
||||
context_kwds = ("encoding",)
|
||||
default_encoding = "utf-8"
|
||||
|
||||
@classmethod
|
||||
def identify(cls, hash):
|
||||
if isinstance(hash, unicode_or_bytes_types):
|
||||
return True
|
||||
else:
|
||||
raise uh.exc.ExpectedStringError(hash, "hash")
|
||||
|
||||
@classmethod
|
||||
def hash(cls, secret, encoding=None):
|
||||
uh.validate_secret(secret)
|
||||
if not encoding:
|
||||
encoding = cls.default_encoding
|
||||
return to_native_str(secret, encoding, "secret")
|
||||
|
||||
@classmethod
|
||||
def verify(cls, secret, hash, encoding=None):
|
||||
if not encoding:
|
||||
encoding = cls.default_encoding
|
||||
hash = to_native_str(hash, encoding, "hash")
|
||||
if not cls.identify(hash):
|
||||
raise uh.exc.InvalidHashError(cls)
|
||||
return str_consteq(cls.hash(secret, encoding), hash)
|
||||
|
||||
@uh.deprecated_method(deprecated="1.7", removed="2.0")
|
||||
@classmethod
|
||||
def genconfig(cls):
|
||||
return cls.hash("")
|
||||
|
||||
@uh.deprecated_method(deprecated="1.7", removed="2.0")
|
||||
@classmethod
|
||||
def genhash(cls, secret, config, encoding=None):
|
||||
# NOTE: 'config' is ignored, as this hash has no salting / etc
|
||||
if not cls.identify(config):
|
||||
raise uh.exc.InvalidHashError(cls)
|
||||
return cls.hash(secret, encoding=encoding)
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
244
venv/lib/python3.12/site-packages/passlib/handlers/mssql.py
Normal file
244
venv/lib/python3.12/site-packages/passlib/handlers/mssql.py
Normal file
@@ -0,0 +1,244 @@
|
||||
"""passlib.handlers.mssql - MS-SQL Password Hash
|
||||
|
||||
Notes
|
||||
=====
|
||||
MS-SQL has used a number of hash algs over the years,
|
||||
most of which were exposed through the undocumented
|
||||
'pwdencrypt' and 'pwdcompare' sql functions.
|
||||
|
||||
Known formats
|
||||
-------------
|
||||
6.5
|
||||
snefru hash, ascii encoded password
|
||||
no examples found
|
||||
|
||||
7.0
|
||||
snefru hash, unicode (what encoding?)
|
||||
saw ref that these blobs were 16 bytes in size
|
||||
no examples found
|
||||
|
||||
2000
|
||||
byte string using displayed as 0x hex, using 0x0100 prefix.
|
||||
contains hashes of password and upper-case password.
|
||||
|
||||
2007
|
||||
same as 2000, but without the upper-case hash.
|
||||
|
||||
refs
|
||||
----------
|
||||
https://blogs.msdn.com/b/lcris/archive/2007/04/30/sql-server-2005-about-login-password-hashes.aspx?Redirected=true
|
||||
http://us.generation-nt.com/securing-passwords-hash-help-35429432.html
|
||||
http://forum.md5decrypter.co.uk/topic230-mysql-and-mssql-get-password-hashes.aspx
|
||||
http://www.theregister.co.uk/2002/07/08/cracking_ms_sql_server_passwords/
|
||||
"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
# core
|
||||
from binascii import hexlify, unhexlify
|
||||
from hashlib import sha1
|
||||
import re
|
||||
import logging; log = logging.getLogger(__name__)
|
||||
from warnings import warn
|
||||
# site
|
||||
# pkg
|
||||
from passlib.utils import consteq
|
||||
from passlib.utils.compat import bascii_to_str, unicode, u
|
||||
import passlib.utils.handlers as uh
|
||||
# local
|
||||
__all__ = [
|
||||
"mssql2000",
|
||||
"mssql2005",
|
||||
]
|
||||
|
||||
#=============================================================================
|
||||
# mssql 2000
|
||||
#=============================================================================
|
||||
def _raw_mssql(secret, salt):
|
||||
assert isinstance(secret, unicode)
|
||||
assert isinstance(salt, bytes)
|
||||
return sha1(secret.encode("utf-16-le") + salt).digest()
|
||||
|
||||
BIDENT = b"0x0100"
|
||||
##BIDENT2 = b("\x01\x00")
|
||||
UIDENT = u("0x0100")
|
||||
|
||||
def _ident_mssql(hash, csize, bsize):
|
||||
"""common identify for mssql 2000/2005"""
|
||||
if isinstance(hash, unicode):
|
||||
if len(hash) == csize and hash.startswith(UIDENT):
|
||||
return True
|
||||
elif isinstance(hash, bytes):
|
||||
if len(hash) == csize and hash.startswith(BIDENT):
|
||||
return True
|
||||
##elif len(hash) == bsize and hash.startswith(BIDENT2): # raw bytes
|
||||
## return True
|
||||
else:
|
||||
raise uh.exc.ExpectedStringError(hash, "hash")
|
||||
return False
|
||||
|
||||
def _parse_mssql(hash, csize, bsize, handler):
|
||||
"""common parser for mssql 2000/2005; returns 4 byte salt + checksum"""
|
||||
if isinstance(hash, unicode):
|
||||
if len(hash) == csize and hash.startswith(UIDENT):
|
||||
try:
|
||||
return unhexlify(hash[6:].encode("utf-8"))
|
||||
except TypeError: # throw when bad char found
|
||||
pass
|
||||
elif isinstance(hash, bytes):
|
||||
# assumes ascii-compat encoding
|
||||
assert isinstance(hash, bytes)
|
||||
if len(hash) == csize and hash.startswith(BIDENT):
|
||||
try:
|
||||
return unhexlify(hash[6:])
|
||||
except TypeError: # throw when bad char found
|
||||
pass
|
||||
##elif len(hash) == bsize and hash.startswith(BIDENT2): # raw bytes
|
||||
## return hash[2:]
|
||||
else:
|
||||
raise uh.exc.ExpectedStringError(hash, "hash")
|
||||
raise uh.exc.InvalidHashError(handler)
|
||||
|
||||
class mssql2000(uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
|
||||
"""This class implements the password hash used by MS-SQL 2000, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports a fixed-length salt.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
|
||||
|
||||
:type salt: bytes
|
||||
:param salt:
|
||||
Optional salt string.
|
||||
If not specified, one will be autogenerated (this is recommended).
|
||||
If specified, it must be 4 bytes in length.
|
||||
|
||||
:type relaxed: bool
|
||||
:param relaxed:
|
||||
By default, providing an invalid value for one of the other
|
||||
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
|
||||
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
|
||||
will be issued instead. Correctable errors include
|
||||
``salt`` strings that are too long.
|
||||
"""
|
||||
#===================================================================
|
||||
# algorithm information
|
||||
#===================================================================
|
||||
name = "mssql2000"
|
||||
setting_kwds = ("salt",)
|
||||
checksum_size = 40
|
||||
min_salt_size = max_salt_size = 4
|
||||
|
||||
#===================================================================
|
||||
# formatting
|
||||
#===================================================================
|
||||
|
||||
# 0100 - 2 byte identifier
|
||||
# 4 byte salt
|
||||
# 20 byte checksum
|
||||
# 20 byte checksum
|
||||
# = 46 bytes
|
||||
# encoded '0x' + 92 chars = 94
|
||||
|
||||
@classmethod
|
||||
def identify(cls, hash):
|
||||
return _ident_mssql(hash, 94, 46)
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, hash):
|
||||
data = _parse_mssql(hash, 94, 46, cls)
|
||||
return cls(salt=data[:4], checksum=data[4:])
|
||||
|
||||
def to_string(self):
|
||||
raw = self.salt + self.checksum
|
||||
# raw bytes format - BIDENT2 + raw
|
||||
return "0x0100" + bascii_to_str(hexlify(raw).upper())
|
||||
|
||||
def _calc_checksum(self, secret):
|
||||
if isinstance(secret, bytes):
|
||||
secret = secret.decode("utf-8")
|
||||
salt = self.salt
|
||||
return _raw_mssql(secret, salt) + _raw_mssql(secret.upper(), salt)
|
||||
|
||||
@classmethod
|
||||
def verify(cls, secret, hash):
|
||||
# NOTE: we only compare against the upper-case hash
|
||||
# XXX: add 'full' just to verify both checksums?
|
||||
uh.validate_secret(secret)
|
||||
self = cls.from_string(hash)
|
||||
chk = self.checksum
|
||||
if chk is None:
|
||||
raise uh.exc.MissingDigestError(cls)
|
||||
if isinstance(secret, bytes):
|
||||
secret = secret.decode("utf-8")
|
||||
result = _raw_mssql(secret.upper(), self.salt)
|
||||
return consteq(result, chk[20:])
|
||||
|
||||
#=============================================================================
|
||||
# handler
|
||||
#=============================================================================
|
||||
class mssql2005(uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
|
||||
"""This class implements the password hash used by MS-SQL 2005, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports a fixed-length salt.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
|
||||
|
||||
:type salt: bytes
|
||||
:param salt:
|
||||
Optional salt string.
|
||||
If not specified, one will be autogenerated (this is recommended).
|
||||
If specified, it must be 4 bytes in length.
|
||||
|
||||
:type relaxed: bool
|
||||
:param relaxed:
|
||||
By default, providing an invalid value for one of the other
|
||||
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
|
||||
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
|
||||
will be issued instead. Correctable errors include
|
||||
``salt`` strings that are too long.
|
||||
"""
|
||||
#===================================================================
|
||||
# algorithm information
|
||||
#===================================================================
|
||||
name = "mssql2005"
|
||||
setting_kwds = ("salt",)
|
||||
|
||||
checksum_size = 20
|
||||
min_salt_size = max_salt_size = 4
|
||||
|
||||
#===================================================================
|
||||
# formatting
|
||||
#===================================================================
|
||||
|
||||
# 0x0100 - 2 byte identifier
|
||||
# 4 byte salt
|
||||
# 20 byte checksum
|
||||
# = 26 bytes
|
||||
# encoded '0x' + 52 chars = 54
|
||||
|
||||
@classmethod
|
||||
def identify(cls, hash):
|
||||
return _ident_mssql(hash, 54, 26)
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, hash):
|
||||
data = _parse_mssql(hash, 54, 26, cls)
|
||||
return cls(salt=data[:4], checksum=data[4:])
|
||||
|
||||
def to_string(self):
|
||||
raw = self.salt + self.checksum
|
||||
# raw bytes format - BIDENT2 + raw
|
||||
return "0x0100" + bascii_to_str(hexlify(raw)).upper()
|
||||
|
||||
def _calc_checksum(self, secret):
|
||||
if isinstance(secret, bytes):
|
||||
secret = secret.decode("utf-8")
|
||||
return _raw_mssql(secret, self.salt)
|
||||
|
||||
#===================================================================
|
||||
# eoc
|
||||
#===================================================================
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
128
venv/lib/python3.12/site-packages/passlib/handlers/mysql.py
Normal file
128
venv/lib/python3.12/site-packages/passlib/handlers/mysql.py
Normal file
@@ -0,0 +1,128 @@
|
||||
"""passlib.handlers.mysql
|
||||
|
||||
MySQL 3.2.3 / OLD_PASSWORD()
|
||||
|
||||
This implements Mysql's OLD_PASSWORD algorithm, introduced in version 3.2.3, deprecated in version 4.1.
|
||||
|
||||
See :mod:`passlib.handlers.mysql_41` for the new algorithm was put in place in version 4.1
|
||||
|
||||
This algorithm is known to be very insecure, and should only be used to verify existing password hashes.
|
||||
|
||||
http://djangosnippets.org/snippets/1508/
|
||||
|
||||
MySQL 4.1.1 / NEW PASSWORD
|
||||
This implements Mysql new PASSWORD algorithm, introduced in version 4.1.
|
||||
|
||||
This function is unsalted, and therefore not very secure against rainbow attacks.
|
||||
It should only be used when dealing with mysql passwords,
|
||||
for all other purposes, you should use a salted hash function.
|
||||
|
||||
Description taken from http://dev.mysql.com/doc/refman/6.0/en/password-hashing.html
|
||||
"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
# core
|
||||
from hashlib import sha1
|
||||
import re
|
||||
import logging; log = logging.getLogger(__name__)
|
||||
from warnings import warn
|
||||
# site
|
||||
# pkg
|
||||
from passlib.utils import to_native_str
|
||||
from passlib.utils.compat import bascii_to_str, unicode, u, \
|
||||
byte_elem_value, str_to_uascii
|
||||
import passlib.utils.handlers as uh
|
||||
# local
|
||||
__all__ = [
|
||||
'mysql323',
|
||||
'mysq41',
|
||||
]
|
||||
|
||||
#=============================================================================
|
||||
# backend
|
||||
#=============================================================================
|
||||
class mysql323(uh.StaticHandler):
|
||||
"""This class implements the MySQL 3.2.3 password hash, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It has no salt and a single fixed round.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept no optional keywords.
|
||||
"""
|
||||
#===================================================================
|
||||
# class attrs
|
||||
#===================================================================
|
||||
name = "mysql323"
|
||||
checksum_size = 16
|
||||
checksum_chars = uh.HEX_CHARS
|
||||
|
||||
#===================================================================
|
||||
# methods
|
||||
#===================================================================
|
||||
@classmethod
|
||||
def _norm_hash(cls, hash):
|
||||
return hash.lower()
|
||||
|
||||
def _calc_checksum(self, secret):
|
||||
# FIXME: no idea if mysql has a policy about handling unicode passwords
|
||||
if isinstance(secret, unicode):
|
||||
secret = secret.encode("utf-8")
|
||||
|
||||
MASK_32 = 0xffffffff
|
||||
MASK_31 = 0x7fffffff
|
||||
WHITE = b' \t'
|
||||
|
||||
nr1 = 0x50305735
|
||||
nr2 = 0x12345671
|
||||
add = 7
|
||||
for c in secret:
|
||||
if c in WHITE:
|
||||
continue
|
||||
tmp = byte_elem_value(c)
|
||||
nr1 ^= ((((nr1 & 63)+add)*tmp) + (nr1 << 8)) & MASK_32
|
||||
nr2 = (nr2+((nr2 << 8) ^ nr1)) & MASK_32
|
||||
add = (add+tmp) & MASK_32
|
||||
return u("%08x%08x") % (nr1 & MASK_31, nr2 & MASK_31)
|
||||
|
||||
#===================================================================
|
||||
# eoc
|
||||
#===================================================================
|
||||
|
||||
#=============================================================================
|
||||
# handler
|
||||
#=============================================================================
|
||||
class mysql41(uh.StaticHandler):
|
||||
"""This class implements the MySQL 4.1 password hash, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It has no salt and a single fixed round.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept no optional keywords.
|
||||
"""
|
||||
#===================================================================
|
||||
# class attrs
|
||||
#===================================================================
|
||||
name = "mysql41"
|
||||
_hash_prefix = u("*")
|
||||
checksum_chars = uh.HEX_CHARS
|
||||
checksum_size = 40
|
||||
|
||||
#===================================================================
|
||||
# methods
|
||||
#===================================================================
|
||||
@classmethod
|
||||
def _norm_hash(cls, hash):
|
||||
return hash.upper()
|
||||
|
||||
def _calc_checksum(self, secret):
|
||||
# FIXME: no idea if mysql has a policy about handling unicode passwords
|
||||
if isinstance(secret, unicode):
|
||||
secret = secret.encode("utf-8")
|
||||
return str_to_uascii(sha1(sha1(secret).digest()).hexdigest()).upper()
|
||||
|
||||
#===================================================================
|
||||
# eoc
|
||||
#===================================================================
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
172
venv/lib/python3.12/site-packages/passlib/handlers/oracle.py
Normal file
172
venv/lib/python3.12/site-packages/passlib/handlers/oracle.py
Normal file
@@ -0,0 +1,172 @@
|
||||
"""passlib.handlers.oracle - Oracle DB Password Hashes"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
# core
|
||||
from binascii import hexlify, unhexlify
|
||||
from hashlib import sha1
|
||||
import re
|
||||
import logging; log = logging.getLogger(__name__)
|
||||
# site
|
||||
# pkg
|
||||
from passlib.utils import to_unicode, xor_bytes
|
||||
from passlib.utils.compat import irange, u, \
|
||||
uascii_to_str, unicode, str_to_uascii
|
||||
from passlib.crypto.des import des_encrypt_block
|
||||
import passlib.utils.handlers as uh
|
||||
# local
|
||||
__all__ = [
|
||||
"oracle10g",
|
||||
"oracle11g"
|
||||
]
|
||||
|
||||
#=============================================================================
|
||||
# oracle10
|
||||
#=============================================================================
|
||||
def des_cbc_encrypt(key, value, iv=b'\x00' * 8, pad=b'\x00'):
|
||||
"""performs des-cbc encryption, returns only last block.
|
||||
|
||||
this performs a specific DES-CBC encryption implementation
|
||||
as needed by the Oracle10 hash. it probably won't be useful for
|
||||
other purposes as-is.
|
||||
|
||||
input value is null-padded to multiple of 8 bytes.
|
||||
|
||||
:arg key: des key as bytes
|
||||
:arg value: value to encrypt, as bytes.
|
||||
:param iv: optional IV
|
||||
:param pad: optional pad byte
|
||||
|
||||
:returns: last block of DES-CBC encryption of all ``value``'s byte blocks.
|
||||
"""
|
||||
value += pad * (-len(value) % 8) # null pad to multiple of 8
|
||||
hash = iv # start things off
|
||||
for offset in irange(0,len(value),8):
|
||||
chunk = xor_bytes(hash, value[offset:offset+8])
|
||||
hash = des_encrypt_block(key, chunk)
|
||||
return hash
|
||||
|
||||
# magic string used as initial des key by oracle10
|
||||
ORACLE10_MAGIC = b"\x01\x23\x45\x67\x89\xAB\xCD\xEF"
|
||||
|
||||
class oracle10(uh.HasUserContext, uh.StaticHandler):
|
||||
"""This class implements the password hash used by Oracle up to version 10g, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It does a single round of hashing, and relies on the username as the salt.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.hash`, :meth:`~passlib.ifc.PasswordHash.genhash`, and :meth:`~passlib.ifc.PasswordHash.verify` methods all require the
|
||||
following additional contextual keywords:
|
||||
|
||||
:type user: str
|
||||
:param user: name of oracle user account this password is associated with.
|
||||
"""
|
||||
#===================================================================
|
||||
# algorithm information
|
||||
#===================================================================
|
||||
name = "oracle10"
|
||||
checksum_chars = uh.HEX_CHARS
|
||||
checksum_size = 16
|
||||
|
||||
#===================================================================
|
||||
# methods
|
||||
#===================================================================
|
||||
@classmethod
|
||||
def _norm_hash(cls, hash):
|
||||
return hash.upper()
|
||||
|
||||
def _calc_checksum(self, secret):
|
||||
# FIXME: not sure how oracle handles unicode.
|
||||
# online docs about 10g hash indicate it puts ascii chars
|
||||
# in a 2-byte encoding w/ the high byte set to null.
|
||||
# they don't say how it handles other chars, or what encoding.
|
||||
#
|
||||
# so for now, encoding secret & user to utf-16-be,
|
||||
# since that fits, and if secret/user is bytes,
|
||||
# we assume utf-8, and decode first.
|
||||
#
|
||||
# this whole mess really needs someone w/ an oracle system,
|
||||
# and some answers :)
|
||||
if isinstance(secret, bytes):
|
||||
secret = secret.decode("utf-8")
|
||||
user = to_unicode(self.user, "utf-8", param="user")
|
||||
input = (user+secret).upper().encode("utf-16-be")
|
||||
hash = des_cbc_encrypt(ORACLE10_MAGIC, input)
|
||||
hash = des_cbc_encrypt(hash, input)
|
||||
return hexlify(hash).decode("ascii").upper()
|
||||
|
||||
#===================================================================
|
||||
# eoc
|
||||
#===================================================================
|
||||
|
||||
#=============================================================================
|
||||
# oracle11
|
||||
#=============================================================================
|
||||
class oracle11(uh.HasSalt, uh.GenericHandler):
|
||||
"""This class implements the Oracle11g password hash, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports a fixed-length salt.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
|
||||
|
||||
:type salt: str
|
||||
:param salt:
|
||||
Optional salt string.
|
||||
If not specified, one will be autogenerated (this is recommended).
|
||||
If specified, it must be 20 hexadecimal characters.
|
||||
|
||||
:type relaxed: bool
|
||||
:param relaxed:
|
||||
By default, providing an invalid value for one of the other
|
||||
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
|
||||
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
|
||||
will be issued instead. Correctable errors include
|
||||
``salt`` strings that are too long.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
"""
|
||||
#===================================================================
|
||||
# class attrs
|
||||
#===================================================================
|
||||
#--GenericHandler--
|
||||
name = "oracle11"
|
||||
setting_kwds = ("salt",)
|
||||
checksum_size = 40
|
||||
checksum_chars = uh.UPPER_HEX_CHARS
|
||||
|
||||
#--HasSalt--
|
||||
min_salt_size = max_salt_size = 20
|
||||
salt_chars = uh.UPPER_HEX_CHARS
|
||||
|
||||
|
||||
#===================================================================
|
||||
# methods
|
||||
#===================================================================
|
||||
_hash_regex = re.compile(u("^S:(?P<chk>[0-9a-f]{40})(?P<salt>[0-9a-f]{20})$"), re.I)
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, hash):
|
||||
hash = to_unicode(hash, "ascii", "hash")
|
||||
m = cls._hash_regex.match(hash)
|
||||
if not m:
|
||||
raise uh.exc.InvalidHashError(cls)
|
||||
salt, chk = m.group("salt", "chk")
|
||||
return cls(salt=salt, checksum=chk.upper())
|
||||
|
||||
def to_string(self):
|
||||
chk = self.checksum
|
||||
hash = u("S:%s%s") % (chk.upper(), self.salt.upper())
|
||||
return uascii_to_str(hash)
|
||||
|
||||
def _calc_checksum(self, secret):
|
||||
if isinstance(secret, unicode):
|
||||
secret = secret.encode("utf-8")
|
||||
chk = sha1(secret + unhexlify(self.salt.encode("ascii"))).hexdigest()
|
||||
return str_to_uascii(chk).upper()
|
||||
|
||||
#===================================================================
|
||||
# eoc
|
||||
#===================================================================
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
475
venv/lib/python3.12/site-packages/passlib/handlers/pbkdf2.py
Normal file
475
venv/lib/python3.12/site-packages/passlib/handlers/pbkdf2.py
Normal file
@@ -0,0 +1,475 @@
|
||||
"""passlib.handlers.pbkdf - PBKDF2 based hashes"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
# core
|
||||
from binascii import hexlify, unhexlify
|
||||
from base64 import b64encode, b64decode
|
||||
import logging; log = logging.getLogger(__name__)
|
||||
# site
|
||||
# pkg
|
||||
from passlib.utils import to_unicode
|
||||
from passlib.utils.binary import ab64_decode, ab64_encode
|
||||
from passlib.utils.compat import str_to_bascii, u, uascii_to_str, unicode
|
||||
from passlib.crypto.digest import pbkdf2_hmac
|
||||
import passlib.utils.handlers as uh
|
||||
# local
|
||||
__all__ = [
|
||||
"pbkdf2_sha1",
|
||||
"pbkdf2_sha256",
|
||||
"pbkdf2_sha512",
|
||||
"cta_pbkdf2_sha1",
|
||||
"dlitz_pbkdf2_sha1",
|
||||
"grub_pbkdf2_sha512",
|
||||
]
|
||||
|
||||
#=============================================================================
|
||||
#
|
||||
#=============================================================================
|
||||
class Pbkdf2DigestHandler(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
|
||||
"""base class for various pbkdf2_{digest} algorithms"""
|
||||
#===================================================================
|
||||
# class attrs
|
||||
#===================================================================
|
||||
|
||||
#--GenericHandler--
|
||||
setting_kwds = ("salt", "salt_size", "rounds")
|
||||
checksum_chars = uh.HASH64_CHARS
|
||||
|
||||
#--HasSalt--
|
||||
default_salt_size = 16
|
||||
max_salt_size = 1024
|
||||
|
||||
#--HasRounds--
|
||||
default_rounds = None # set by subclass
|
||||
min_rounds = 1
|
||||
max_rounds = 0xffffffff # setting at 32-bit limit for now
|
||||
rounds_cost = "linear"
|
||||
|
||||
#--this class--
|
||||
_digest = None # name of subclass-specified hash
|
||||
|
||||
# NOTE: max_salt_size and max_rounds are arbitrarily chosen to provide sanity check.
|
||||
# the underlying pbkdf2 specifies no bounds for either.
|
||||
|
||||
# NOTE: defaults chosen to be at least as large as pbkdf2 rfc recommends...
|
||||
# >8 bytes of entropy in salt, >1000 rounds
|
||||
# increased due to time since rfc established
|
||||
|
||||
#===================================================================
|
||||
# methods
|
||||
#===================================================================
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, hash):
|
||||
rounds, salt, chk = uh.parse_mc3(hash, cls.ident, handler=cls)
|
||||
salt = ab64_decode(salt.encode("ascii"))
|
||||
if chk:
|
||||
chk = ab64_decode(chk.encode("ascii"))
|
||||
return cls(rounds=rounds, salt=salt, checksum=chk)
|
||||
|
||||
def to_string(self):
|
||||
salt = ab64_encode(self.salt).decode("ascii")
|
||||
chk = ab64_encode(self.checksum).decode("ascii")
|
||||
return uh.render_mc3(self.ident, self.rounds, salt, chk)
|
||||
|
||||
def _calc_checksum(self, secret):
|
||||
# NOTE: pbkdf2_hmac() will encode secret & salt using UTF8
|
||||
return pbkdf2_hmac(self._digest, secret, self.salt, self.rounds, self.checksum_size)
|
||||
|
||||
def create_pbkdf2_hash(hash_name, digest_size, rounds=12000, ident=None, module=__name__):
|
||||
"""create new Pbkdf2DigestHandler subclass for a specific hash"""
|
||||
name = 'pbkdf2_' + hash_name
|
||||
if ident is None:
|
||||
ident = u("$pbkdf2-%s$") % (hash_name,)
|
||||
base = Pbkdf2DigestHandler
|
||||
return type(name, (base,), dict(
|
||||
__module__=module, # so ABCMeta won't clobber it.
|
||||
name=name,
|
||||
ident=ident,
|
||||
_digest = hash_name,
|
||||
default_rounds=rounds,
|
||||
checksum_size=digest_size,
|
||||
encoded_checksum_size=(digest_size*4+2)//3,
|
||||
__doc__="""This class implements a generic ``PBKDF2-HMAC-%(digest)s``-based password hash, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports a variable-length salt, and a variable number of rounds.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
|
||||
|
||||
:type salt: bytes
|
||||
:param salt:
|
||||
Optional salt bytes.
|
||||
If specified, the length must be between 0-1024 bytes.
|
||||
If not specified, a %(dsc)d byte salt will be autogenerated (this is recommended).
|
||||
|
||||
:type salt_size: int
|
||||
:param salt_size:
|
||||
Optional number of bytes to use when autogenerating new salts.
|
||||
Defaults to %(dsc)d bytes, but can be any value between 0 and 1024.
|
||||
|
||||
:type rounds: int
|
||||
:param rounds:
|
||||
Optional number of rounds to use.
|
||||
Defaults to %(dr)d, but must be within ``range(1,1<<32)``.
|
||||
|
||||
:type relaxed: bool
|
||||
:param relaxed:
|
||||
By default, providing an invalid value for one of the other
|
||||
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
|
||||
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
|
||||
will be issued instead. Correctable errors include ``rounds``
|
||||
that are too small or too large, and ``salt`` strings that are too long.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
""" % dict(digest=hash_name.upper(), dsc=base.default_salt_size, dr=rounds)
|
||||
))
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
# derived handlers
|
||||
#------------------------------------------------------------------------
|
||||
pbkdf2_sha1 = create_pbkdf2_hash("sha1", 20, 131000, ident=u("$pbkdf2$"))
|
||||
pbkdf2_sha256 = create_pbkdf2_hash("sha256", 32, 29000)
|
||||
pbkdf2_sha512 = create_pbkdf2_hash("sha512", 64, 25000)
|
||||
|
||||
ldap_pbkdf2_sha1 = uh.PrefixWrapper("ldap_pbkdf2_sha1", pbkdf2_sha1, "{PBKDF2}", "$pbkdf2$", ident=True)
|
||||
ldap_pbkdf2_sha256 = uh.PrefixWrapper("ldap_pbkdf2_sha256", pbkdf2_sha256, "{PBKDF2-SHA256}", "$pbkdf2-sha256$", ident=True)
|
||||
ldap_pbkdf2_sha512 = uh.PrefixWrapper("ldap_pbkdf2_sha512", pbkdf2_sha512, "{PBKDF2-SHA512}", "$pbkdf2-sha512$", ident=True)
|
||||
|
||||
#=============================================================================
|
||||
# cryptacular's pbkdf2 hash
|
||||
#=============================================================================
|
||||
|
||||
# bytes used by cta hash for base64 values 63 & 64
|
||||
CTA_ALTCHARS = b"-_"
|
||||
|
||||
class cta_pbkdf2_sha1(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
|
||||
"""This class implements Cryptacular's PBKDF2-based crypt algorithm, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports a variable-length salt, and a variable number of rounds.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
|
||||
|
||||
:type salt: bytes
|
||||
:param salt:
|
||||
Optional salt bytes.
|
||||
If specified, it may be any length.
|
||||
If not specified, a one will be autogenerated (this is recommended).
|
||||
|
||||
:type salt_size: int
|
||||
:param salt_size:
|
||||
Optional number of bytes to use when autogenerating new salts.
|
||||
Defaults to 16 bytes, but can be any value between 0 and 1024.
|
||||
|
||||
:type rounds: int
|
||||
:param rounds:
|
||||
Optional number of rounds to use.
|
||||
Defaults to 60000, must be within ``range(1,1<<32)``.
|
||||
|
||||
:type relaxed: bool
|
||||
:param relaxed:
|
||||
By default, providing an invalid value for one of the other
|
||||
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
|
||||
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
|
||||
will be issued instead. Correctable errors include ``rounds``
|
||||
that are too small or too large, and ``salt`` strings that are too long.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
"""
|
||||
|
||||
#===================================================================
|
||||
# class attrs
|
||||
#===================================================================
|
||||
#--GenericHandler--
|
||||
name = "cta_pbkdf2_sha1"
|
||||
setting_kwds = ("salt", "salt_size", "rounds")
|
||||
ident = u("$p5k2$")
|
||||
checksum_size = 20
|
||||
|
||||
# NOTE: max_salt_size and max_rounds are arbitrarily chosen to provide a
|
||||
# sanity check. underlying algorithm (and reference implementation)
|
||||
# allows effectively unbounded values for both of these parameters.
|
||||
|
||||
#--HasSalt--
|
||||
default_salt_size = 16
|
||||
max_salt_size = 1024
|
||||
|
||||
#--HasRounds--
|
||||
default_rounds = pbkdf2_sha1.default_rounds
|
||||
min_rounds = 1
|
||||
max_rounds = 0xffffffff # setting at 32-bit limit for now
|
||||
rounds_cost = "linear"
|
||||
|
||||
#===================================================================
|
||||
# formatting
|
||||
#===================================================================
|
||||
|
||||
# hash $p5k2$1000$ZxK4ZBJCfQg=$jJZVscWtO--p1-xIZl6jhO2LKR0=
|
||||
# ident $p5k2$
|
||||
# rounds 1000
|
||||
# salt ZxK4ZBJCfQg=
|
||||
# chk jJZVscWtO--p1-xIZl6jhO2LKR0=
|
||||
# NOTE: rounds in hex
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, hash):
|
||||
# NOTE: passlib deviation - forbidding zero-padded rounds
|
||||
rounds, salt, chk = uh.parse_mc3(hash, cls.ident, rounds_base=16, handler=cls)
|
||||
salt = b64decode(salt.encode("ascii"), CTA_ALTCHARS)
|
||||
if chk:
|
||||
chk = b64decode(chk.encode("ascii"), CTA_ALTCHARS)
|
||||
return cls(rounds=rounds, salt=salt, checksum=chk)
|
||||
|
||||
def to_string(self):
|
||||
salt = b64encode(self.salt, CTA_ALTCHARS).decode("ascii")
|
||||
chk = b64encode(self.checksum, CTA_ALTCHARS).decode("ascii")
|
||||
return uh.render_mc3(self.ident, self.rounds, salt, chk, rounds_base=16)
|
||||
|
||||
#===================================================================
|
||||
# backend
|
||||
#===================================================================
|
||||
def _calc_checksum(self, secret):
|
||||
# NOTE: pbkdf2_hmac() will encode secret & salt using utf-8
|
||||
return pbkdf2_hmac("sha1", secret, self.salt, self.rounds, 20)
|
||||
|
||||
#===================================================================
|
||||
# eoc
|
||||
#===================================================================
|
||||
|
||||
#=============================================================================
|
||||
# dlitz's pbkdf2 hash
|
||||
#=============================================================================
|
||||
class dlitz_pbkdf2_sha1(uh.HasRounds, uh.HasSalt, uh.GenericHandler):
|
||||
"""This class implements Dwayne Litzenberger's PBKDF2-based crypt algorithm, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports a variable-length salt, and a variable number of rounds.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
|
||||
|
||||
:type salt: str
|
||||
:param salt:
|
||||
Optional salt string.
|
||||
If specified, it may be any length, but must use the characters in the regexp range ``[./0-9A-Za-z]``.
|
||||
If not specified, a 16 character salt will be autogenerated (this is recommended).
|
||||
|
||||
:type salt_size: int
|
||||
:param salt_size:
|
||||
Optional number of bytes to use when autogenerating new salts.
|
||||
Defaults to 16 bytes, but can be any value between 0 and 1024.
|
||||
|
||||
:type rounds: int
|
||||
:param rounds:
|
||||
Optional number of rounds to use.
|
||||
Defaults to 60000, must be within ``range(1,1<<32)``.
|
||||
|
||||
:type relaxed: bool
|
||||
:param relaxed:
|
||||
By default, providing an invalid value for one of the other
|
||||
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
|
||||
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
|
||||
will be issued instead. Correctable errors include ``rounds``
|
||||
that are too small or too large, and ``salt`` strings that are too long.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
"""
|
||||
|
||||
#===================================================================
|
||||
# class attrs
|
||||
#===================================================================
|
||||
#--GenericHandler--
|
||||
name = "dlitz_pbkdf2_sha1"
|
||||
setting_kwds = ("salt", "salt_size", "rounds")
|
||||
ident = u("$p5k2$")
|
||||
_stub_checksum = u("0" * 48 + "=")
|
||||
|
||||
# NOTE: max_salt_size and max_rounds are arbitrarily chosen to provide a
|
||||
# sanity check. underlying algorithm (and reference implementation)
|
||||
# allows effectively unbounded values for both of these parameters.
|
||||
|
||||
#--HasSalt--
|
||||
default_salt_size = 16
|
||||
max_salt_size = 1024
|
||||
salt_chars = uh.HASH64_CHARS
|
||||
|
||||
#--HasRounds--
|
||||
# NOTE: for security, the default here is set to match pbkdf2_sha1,
|
||||
# even though this hash's extra block makes it twice as slow.
|
||||
default_rounds = pbkdf2_sha1.default_rounds
|
||||
min_rounds = 1
|
||||
max_rounds = 0xffffffff # setting at 32-bit limit for now
|
||||
rounds_cost = "linear"
|
||||
|
||||
#===================================================================
|
||||
# formatting
|
||||
#===================================================================
|
||||
|
||||
# hash $p5k2$c$u9HvcT4d$Sd1gwSVCLZYAuqZ25piRnbBEoAesaa/g
|
||||
# ident $p5k2$
|
||||
# rounds c
|
||||
# salt u9HvcT4d
|
||||
# chk Sd1gwSVCLZYAuqZ25piRnbBEoAesaa/g
|
||||
# rounds in lowercase hex, no zero padding
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, hash):
|
||||
rounds, salt, chk = uh.parse_mc3(hash, cls.ident, rounds_base=16,
|
||||
default_rounds=400, handler=cls)
|
||||
return cls(rounds=rounds, salt=salt, checksum=chk)
|
||||
|
||||
def to_string(self):
|
||||
rounds = self.rounds
|
||||
if rounds == 400:
|
||||
rounds = None # omit rounds measurement if == 400
|
||||
return uh.render_mc3(self.ident, rounds, self.salt, self.checksum, rounds_base=16)
|
||||
|
||||
def _get_config(self):
|
||||
rounds = self.rounds
|
||||
if rounds == 400:
|
||||
rounds = None # omit rounds measurement if == 400
|
||||
return uh.render_mc3(self.ident, rounds, self.salt, None, rounds_base=16)
|
||||
|
||||
#===================================================================
|
||||
# backend
|
||||
#===================================================================
|
||||
def _calc_checksum(self, secret):
|
||||
# NOTE: pbkdf2_hmac() will encode secret & salt using utf-8
|
||||
salt = self._get_config()
|
||||
result = pbkdf2_hmac("sha1", secret, salt, self.rounds, 24)
|
||||
return ab64_encode(result).decode("ascii")
|
||||
|
||||
#===================================================================
|
||||
# eoc
|
||||
#===================================================================
|
||||
|
||||
#=============================================================================
|
||||
# crowd
|
||||
#=============================================================================
|
||||
class atlassian_pbkdf2_sha1(uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
|
||||
"""This class implements the PBKDF2 hash used by Atlassian.
|
||||
|
||||
It supports a fixed-length salt, and a fixed number of rounds.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
|
||||
|
||||
:type salt: bytes
|
||||
:param salt:
|
||||
Optional salt bytes.
|
||||
If specified, the length must be exactly 16 bytes.
|
||||
If not specified, a salt will be autogenerated (this is recommended).
|
||||
|
||||
:type relaxed: bool
|
||||
:param relaxed:
|
||||
By default, providing an invalid value for one of the other
|
||||
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
|
||||
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
|
||||
will be issued instead. Correctable errors include
|
||||
``salt`` strings that are too long.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
"""
|
||||
#--GenericHandler--
|
||||
name = "atlassian_pbkdf2_sha1"
|
||||
setting_kwds =("salt",)
|
||||
ident = u("{PKCS5S2}")
|
||||
checksum_size = 32
|
||||
|
||||
#--HasRawSalt--
|
||||
min_salt_size = max_salt_size = 16
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, hash):
|
||||
hash = to_unicode(hash, "ascii", "hash")
|
||||
ident = cls.ident
|
||||
if not hash.startswith(ident):
|
||||
raise uh.exc.InvalidHashError(cls)
|
||||
data = b64decode(hash[len(ident):].encode("ascii"))
|
||||
salt, chk = data[:16], data[16:]
|
||||
return cls(salt=salt, checksum=chk)
|
||||
|
||||
def to_string(self):
|
||||
data = self.salt + self.checksum
|
||||
hash = self.ident + b64encode(data).decode("ascii")
|
||||
return uascii_to_str(hash)
|
||||
|
||||
def _calc_checksum(self, secret):
|
||||
# TODO: find out what crowd's policy is re: unicode
|
||||
# crowd seems to use a fixed number of rounds.
|
||||
# NOTE: pbkdf2_hmac() will encode secret & salt using utf-8
|
||||
return pbkdf2_hmac("sha1", secret, self.salt, 10000, 32)
|
||||
|
||||
#=============================================================================
|
||||
# grub
|
||||
#=============================================================================
|
||||
class grub_pbkdf2_sha512(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
|
||||
"""This class implements Grub's pbkdf2-hmac-sha512 hash, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports a variable-length salt, and a variable number of rounds.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
|
||||
|
||||
:type salt: bytes
|
||||
:param salt:
|
||||
Optional salt bytes.
|
||||
If specified, the length must be between 0-1024 bytes.
|
||||
If not specified, a 64 byte salt will be autogenerated (this is recommended).
|
||||
|
||||
:type salt_size: int
|
||||
:param salt_size:
|
||||
Optional number of bytes to use when autogenerating new salts.
|
||||
Defaults to 64 bytes, but can be any value between 0 and 1024.
|
||||
|
||||
:type rounds: int
|
||||
:param rounds:
|
||||
Optional number of rounds to use.
|
||||
Defaults to 19000, but must be within ``range(1,1<<32)``.
|
||||
|
||||
:type relaxed: bool
|
||||
:param relaxed:
|
||||
By default, providing an invalid value for one of the other
|
||||
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
|
||||
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
|
||||
will be issued instead. Correctable errors include ``rounds``
|
||||
that are too small or too large, and ``salt`` strings that are too long.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
"""
|
||||
name = "grub_pbkdf2_sha512"
|
||||
setting_kwds = ("salt", "salt_size", "rounds")
|
||||
|
||||
ident = u("grub.pbkdf2.sha512.")
|
||||
checksum_size = 64
|
||||
|
||||
# NOTE: max_salt_size and max_rounds are arbitrarily chosen to provide a
|
||||
# sanity check. the underlying pbkdf2 specifies no bounds for either,
|
||||
# and it's not clear what grub specifies.
|
||||
|
||||
default_salt_size = 64
|
||||
max_salt_size = 1024
|
||||
|
||||
default_rounds = pbkdf2_sha512.default_rounds
|
||||
min_rounds = 1
|
||||
max_rounds = 0xffffffff # setting at 32-bit limit for now
|
||||
rounds_cost = "linear"
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, hash):
|
||||
rounds, salt, chk = uh.parse_mc3(hash, cls.ident, sep=u("."),
|
||||
handler=cls)
|
||||
salt = unhexlify(salt.encode("ascii"))
|
||||
if chk:
|
||||
chk = unhexlify(chk.encode("ascii"))
|
||||
return cls(rounds=rounds, salt=salt, checksum=chk)
|
||||
|
||||
def to_string(self):
|
||||
salt = hexlify(self.salt).decode("ascii").upper()
|
||||
chk = hexlify(self.checksum).decode("ascii").upper()
|
||||
return uh.render_mc3(self.ident, self.rounds, salt, chk, sep=u("."))
|
||||
|
||||
def _calc_checksum(self, secret):
|
||||
# TODO: find out what grub's policy is re: unicode
|
||||
# NOTE: pbkdf2_hmac() will encode secret & salt using utf-8
|
||||
return pbkdf2_hmac("sha512", secret, self.salt, self.rounds, 64)
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
135
venv/lib/python3.12/site-packages/passlib/handlers/phpass.py
Normal file
135
venv/lib/python3.12/site-packages/passlib/handlers/phpass.py
Normal file
@@ -0,0 +1,135 @@
|
||||
"""passlib.handlers.phpass - PHPass Portable Crypt
|
||||
|
||||
phppass located - http://www.openwall.com/phpass/
|
||||
algorithm described - http://www.openwall.com/articles/PHP-Users-Passwords
|
||||
|
||||
phpass context - blowfish, bsdi_crypt, phpass
|
||||
"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
# core
|
||||
from hashlib import md5
|
||||
import logging; log = logging.getLogger(__name__)
|
||||
# site
|
||||
# pkg
|
||||
from passlib.utils.binary import h64
|
||||
from passlib.utils.compat import u, uascii_to_str, unicode
|
||||
import passlib.utils.handlers as uh
|
||||
# local
|
||||
__all__ = [
|
||||
"phpass",
|
||||
]
|
||||
|
||||
#=============================================================================
|
||||
# phpass
|
||||
#=============================================================================
|
||||
class phpass(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.GenericHandler):
|
||||
"""This class implements the PHPass Portable Hash, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports a fixed-length salt, and a variable number of rounds.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
|
||||
|
||||
:type salt: str
|
||||
:param salt:
|
||||
Optional salt string.
|
||||
If not specified, one will be autogenerated (this is recommended).
|
||||
If specified, it must be 8 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
|
||||
|
||||
:type rounds: int
|
||||
:param rounds:
|
||||
Optional number of rounds to use.
|
||||
Defaults to 19, must be between 7 and 30, inclusive.
|
||||
This value is logarithmic, the actual number of iterations used will be :samp:`2**{rounds}`.
|
||||
|
||||
:type ident: str
|
||||
:param ident:
|
||||
phpBB3 uses ``H`` instead of ``P`` for its identifier,
|
||||
this may be set to ``H`` in order to generate phpBB3 compatible hashes.
|
||||
it defaults to ``P``.
|
||||
|
||||
:type relaxed: bool
|
||||
:param relaxed:
|
||||
By default, providing an invalid value for one of the other
|
||||
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
|
||||
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
|
||||
will be issued instead. Correctable errors include ``rounds``
|
||||
that are too small or too large, and ``salt`` strings that are too long.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
"""
|
||||
|
||||
#===================================================================
|
||||
# class attrs
|
||||
#===================================================================
|
||||
#--GenericHandler--
|
||||
name = "phpass"
|
||||
setting_kwds = ("salt", "rounds", "ident")
|
||||
checksum_chars = uh.HASH64_CHARS
|
||||
|
||||
#--HasSalt--
|
||||
min_salt_size = max_salt_size = 8
|
||||
salt_chars = uh.HASH64_CHARS
|
||||
|
||||
#--HasRounds--
|
||||
default_rounds = 19
|
||||
min_rounds = 7
|
||||
max_rounds = 30
|
||||
rounds_cost = "log2"
|
||||
|
||||
#--HasManyIdents--
|
||||
default_ident = u("$P$")
|
||||
ident_values = (u("$P$"), u("$H$"))
|
||||
ident_aliases = {u("P"):u("$P$"), u("H"):u("$H$")}
|
||||
|
||||
#===================================================================
|
||||
# formatting
|
||||
#===================================================================
|
||||
|
||||
#$P$9IQRaTwmfeRo7ud9Fh4E2PdI0S3r.L0
|
||||
# $P$
|
||||
# 9
|
||||
# IQRaTwmf
|
||||
# eRo7ud9Fh4E2PdI0S3r.L0
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, hash):
|
||||
ident, data = cls._parse_ident(hash)
|
||||
rounds, salt, chk = data[0], data[1:9], data[9:]
|
||||
return cls(
|
||||
ident=ident,
|
||||
rounds=h64.decode_int6(rounds.encode("ascii")),
|
||||
salt=salt,
|
||||
checksum=chk or None,
|
||||
)
|
||||
|
||||
def to_string(self):
|
||||
hash = u("%s%s%s%s") % (self.ident,
|
||||
h64.encode_int6(self.rounds).decode("ascii"),
|
||||
self.salt,
|
||||
self.checksum or u(''))
|
||||
return uascii_to_str(hash)
|
||||
|
||||
#===================================================================
|
||||
# backend
|
||||
#===================================================================
|
||||
def _calc_checksum(self, secret):
|
||||
# FIXME: can't find definitive policy on how phpass handles non-ascii.
|
||||
if isinstance(secret, unicode):
|
||||
secret = secret.encode("utf-8")
|
||||
real_rounds = 1<<self.rounds
|
||||
result = md5(self.salt.encode("ascii") + secret).digest()
|
||||
r = 0
|
||||
while r < real_rounds:
|
||||
result = md5(result + secret).digest()
|
||||
r += 1
|
||||
return h64.encode_bytes(result).decode("ascii")
|
||||
|
||||
#===================================================================
|
||||
# eoc
|
||||
#===================================================================
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
@@ -0,0 +1,55 @@
|
||||
"""passlib.handlers.postgres_md5 - MD5-based algorithm used by Postgres for pg_shadow table"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
# core
|
||||
from hashlib import md5
|
||||
import logging; log = logging.getLogger(__name__)
|
||||
# site
|
||||
# pkg
|
||||
from passlib.utils import to_bytes
|
||||
from passlib.utils.compat import str_to_uascii, unicode, u
|
||||
import passlib.utils.handlers as uh
|
||||
# local
|
||||
__all__ = [
|
||||
"postgres_md5",
|
||||
]
|
||||
|
||||
#=============================================================================
|
||||
# handler
|
||||
#=============================================================================
|
||||
class postgres_md5(uh.HasUserContext, uh.StaticHandler):
|
||||
"""This class implements the Postgres MD5 Password hash, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It does a single round of hashing, and relies on the username as the salt.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.hash`, :meth:`~passlib.ifc.PasswordHash.genhash`, and :meth:`~passlib.ifc.PasswordHash.verify` methods all require the
|
||||
following additional contextual keywords:
|
||||
|
||||
:type user: str
|
||||
:param user: name of postgres user account this password is associated with.
|
||||
"""
|
||||
#===================================================================
|
||||
# algorithm information
|
||||
#===================================================================
|
||||
name = "postgres_md5"
|
||||
_hash_prefix = u("md5")
|
||||
checksum_chars = uh.HEX_CHARS
|
||||
checksum_size = 32
|
||||
|
||||
#===================================================================
|
||||
# primary interface
|
||||
#===================================================================
|
||||
def _calc_checksum(self, secret):
|
||||
if isinstance(secret, unicode):
|
||||
secret = secret.encode("utf-8")
|
||||
user = to_bytes(self.user, "utf-8", param="user")
|
||||
return str_to_uascii(md5(secret + user).hexdigest())
|
||||
|
||||
#===================================================================
|
||||
# eoc
|
||||
#===================================================================
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
@@ -0,0 +1,29 @@
|
||||
"""passlib.handlers.roundup - Roundup issue tracker hashes"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
# core
|
||||
import logging; log = logging.getLogger(__name__)
|
||||
# site
|
||||
# pkg
|
||||
import passlib.utils.handlers as uh
|
||||
from passlib.utils.compat import u
|
||||
# local
|
||||
__all__ = [
|
||||
"roundup_plaintext",
|
||||
"ldap_hex_md5",
|
||||
"ldap_hex_sha1",
|
||||
]
|
||||
#=============================================================================
|
||||
#
|
||||
#=============================================================================
|
||||
roundup_plaintext = uh.PrefixWrapper("roundup_plaintext", "plaintext",
|
||||
prefix=u("{plaintext}"), lazy=True)
|
||||
|
||||
# NOTE: these are here because they're currently only known to be used by roundup
|
||||
ldap_hex_md5 = uh.PrefixWrapper("ldap_hex_md5", "hex_md5", u("{MD5}"), lazy=True)
|
||||
ldap_hex_sha1 = uh.PrefixWrapper("ldap_hex_sha1", "hex_sha1", u("{SHA}"), lazy=True)
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
582
venv/lib/python3.12/site-packages/passlib/handlers/scram.py
Normal file
582
venv/lib/python3.12/site-packages/passlib/handlers/scram.py
Normal file
@@ -0,0 +1,582 @@
|
||||
"""passlib.handlers.scram - hash for SCRAM credential storage"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
# core
|
||||
import logging; log = logging.getLogger(__name__)
|
||||
# site
|
||||
# pkg
|
||||
from passlib.utils import consteq, saslprep, to_native_str, splitcomma
|
||||
from passlib.utils.binary import ab64_decode, ab64_encode
|
||||
from passlib.utils.compat import bascii_to_str, iteritems, u, native_string_types
|
||||
from passlib.crypto.digest import pbkdf2_hmac, norm_hash_name
|
||||
import passlib.utils.handlers as uh
|
||||
# local
|
||||
__all__ = [
|
||||
"scram",
|
||||
]
|
||||
|
||||
#=============================================================================
|
||||
# scram credentials hash
|
||||
#=============================================================================
|
||||
class scram(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
|
||||
"""This class provides a format for storing SCRAM passwords, and follows
|
||||
the :ref:`password-hash-api`.
|
||||
|
||||
It supports a variable-length salt, and a variable number of rounds.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
|
||||
|
||||
:type salt: bytes
|
||||
:param salt:
|
||||
Optional salt bytes.
|
||||
If specified, the length must be between 0-1024 bytes.
|
||||
If not specified, a 12 byte salt will be autogenerated
|
||||
(this is recommended).
|
||||
|
||||
:type salt_size: int
|
||||
:param salt_size:
|
||||
Optional number of bytes to use when autogenerating new salts.
|
||||
Defaults to 12 bytes, but can be any value between 0 and 1024.
|
||||
|
||||
:type rounds: int
|
||||
:param rounds:
|
||||
Optional number of rounds to use.
|
||||
Defaults to 100000, but must be within ``range(1,1<<32)``.
|
||||
|
||||
:type algs: list of strings
|
||||
:param algs:
|
||||
Specify list of digest algorithms to use.
|
||||
|
||||
By default each scram hash will contain digests for SHA-1,
|
||||
SHA-256, and SHA-512. This can be overridden by specify either be a
|
||||
list such as ``["sha-1", "sha-256"]``, or a comma-separated string
|
||||
such as ``"sha-1, sha-256"``. Names are case insensitive, and may
|
||||
use :mod:`!hashlib` or `IANA <http://www.iana.org/assignments/hash-function-text-names>`_
|
||||
hash names.
|
||||
|
||||
:type relaxed: bool
|
||||
:param relaxed:
|
||||
By default, providing an invalid value for one of the other
|
||||
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
|
||||
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
|
||||
will be issued instead. Correctable errors include ``rounds``
|
||||
that are too small or too large, and ``salt`` strings that are too long.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
|
||||
In addition to the standard :ref:`password-hash-api` methods,
|
||||
this class also provides the following methods for manipulating Passlib
|
||||
scram hashes in ways useful for pluging into a SCRAM protocol stack:
|
||||
|
||||
.. automethod:: extract_digest_info
|
||||
.. automethod:: extract_digest_algs
|
||||
.. automethod:: derive_digest
|
||||
"""
|
||||
#===================================================================
|
||||
# class attrs
|
||||
#===================================================================
|
||||
|
||||
# NOTE: unlike most GenericHandler classes, the 'checksum' attr of
|
||||
# ScramHandler is actually a map from digest_name -> digest, so
|
||||
# many of the standard methods have been overridden.
|
||||
|
||||
# NOTE: max_salt_size and max_rounds are arbitrarily chosen to provide
|
||||
# a sanity check; the underlying pbkdf2 specifies no bounds for either.
|
||||
|
||||
#--GenericHandler--
|
||||
name = "scram"
|
||||
setting_kwds = ("salt", "salt_size", "rounds", "algs")
|
||||
ident = u("$scram$")
|
||||
|
||||
#--HasSalt--
|
||||
default_salt_size = 12
|
||||
max_salt_size = 1024
|
||||
|
||||
#--HasRounds--
|
||||
default_rounds = 100000
|
||||
min_rounds = 1
|
||||
max_rounds = 2**32-1
|
||||
rounds_cost = "linear"
|
||||
|
||||
#--custom--
|
||||
|
||||
# default algorithms when creating new hashes.
|
||||
default_algs = ["sha-1", "sha-256", "sha-512"]
|
||||
|
||||
# list of algs verify prefers to use, in order.
|
||||
_verify_algs = ["sha-256", "sha-512", "sha-224", "sha-384", "sha-1"]
|
||||
|
||||
#===================================================================
|
||||
# instance attrs
|
||||
#===================================================================
|
||||
|
||||
# 'checksum' is different from most GenericHandler subclasses,
|
||||
# in that it contains a dict mapping from alg -> digest,
|
||||
# or None if no checksum present.
|
||||
|
||||
# list of algorithms to create/compare digests for.
|
||||
algs = None
|
||||
|
||||
#===================================================================
|
||||
# scram frontend helpers
|
||||
#===================================================================
|
||||
@classmethod
|
||||
def extract_digest_info(cls, hash, alg):
|
||||
"""return (salt, rounds, digest) for specific hash algorithm.
|
||||
|
||||
:type hash: str
|
||||
:arg hash:
|
||||
:class:`!scram` hash stored for desired user
|
||||
|
||||
:type alg: str
|
||||
:arg alg:
|
||||
Name of digest algorithm (e.g. ``"sha-1"``) requested by client.
|
||||
|
||||
This value is run through :func:`~passlib.crypto.digest.norm_hash_name`,
|
||||
so it is case-insensitive, and can be the raw SCRAM
|
||||
mechanism name (e.g. ``"SCRAM-SHA-1"``), the IANA name,
|
||||
or the hashlib name.
|
||||
|
||||
:raises KeyError:
|
||||
If the hash does not contain an entry for the requested digest
|
||||
algorithm.
|
||||
|
||||
:returns:
|
||||
A tuple containing ``(salt, rounds, digest)``,
|
||||
where *digest* matches the raw bytes returned by
|
||||
SCRAM's :func:`Hi` function for the stored password,
|
||||
the provided *salt*, and the iteration count (*rounds*).
|
||||
*salt* and *digest* are both raw (unencoded) bytes.
|
||||
"""
|
||||
# XXX: this could be sped up by writing custom parsing routine
|
||||
# that just picks out relevant digest, and doesn't bother
|
||||
# with full structure validation each time it's called.
|
||||
alg = norm_hash_name(alg, 'iana')
|
||||
self = cls.from_string(hash)
|
||||
chkmap = self.checksum
|
||||
if not chkmap:
|
||||
raise ValueError("scram hash contains no digests")
|
||||
return self.salt, self.rounds, chkmap[alg]
|
||||
|
||||
@classmethod
|
||||
def extract_digest_algs(cls, hash, format="iana"):
|
||||
"""Return names of all algorithms stored in a given hash.
|
||||
|
||||
:type hash: str
|
||||
:arg hash:
|
||||
The :class:`!scram` hash to parse
|
||||
|
||||
:type format: str
|
||||
:param format:
|
||||
This changes the naming convention used by the
|
||||
returned algorithm names. By default the names
|
||||
are IANA-compatible; possible values are ``"iana"`` or ``"hashlib"``.
|
||||
|
||||
:returns:
|
||||
Returns a list of digest algorithms; e.g. ``["sha-1"]``
|
||||
"""
|
||||
# XXX: this could be sped up by writing custom parsing routine
|
||||
# that just picks out relevant names, and doesn't bother
|
||||
# with full structure validation each time it's called.
|
||||
algs = cls.from_string(hash).algs
|
||||
if format == "iana":
|
||||
return algs
|
||||
else:
|
||||
return [norm_hash_name(alg, format) for alg in algs]
|
||||
|
||||
@classmethod
|
||||
def derive_digest(cls, password, salt, rounds, alg):
|
||||
"""helper to create SaltedPassword digest for SCRAM.
|
||||
|
||||
This performs the step in the SCRAM protocol described as::
|
||||
|
||||
SaltedPassword := Hi(Normalize(password), salt, i)
|
||||
|
||||
:type password: unicode or utf-8 bytes
|
||||
:arg password: password to run through digest
|
||||
|
||||
:type salt: bytes
|
||||
:arg salt: raw salt data
|
||||
|
||||
:type rounds: int
|
||||
:arg rounds: number of iterations.
|
||||
|
||||
:type alg: str
|
||||
:arg alg: name of digest to use (e.g. ``"sha-1"``).
|
||||
|
||||
:returns:
|
||||
raw bytes of ``SaltedPassword``
|
||||
"""
|
||||
if isinstance(password, bytes):
|
||||
password = password.decode("utf-8")
|
||||
# NOTE: pbkdf2_hmac() will encode secret & salt using utf-8,
|
||||
# and handle normalizing alg name.
|
||||
return pbkdf2_hmac(alg, saslprep(password), salt, rounds)
|
||||
|
||||
#===================================================================
|
||||
# serialization
|
||||
#===================================================================
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, hash):
|
||||
hash = to_native_str(hash, "ascii", "hash")
|
||||
if not hash.startswith("$scram$"):
|
||||
raise uh.exc.InvalidHashError(cls)
|
||||
parts = hash[7:].split("$")
|
||||
if len(parts) != 3:
|
||||
raise uh.exc.MalformedHashError(cls)
|
||||
rounds_str, salt_str, chk_str = parts
|
||||
|
||||
# decode rounds
|
||||
rounds = int(rounds_str)
|
||||
if rounds_str != str(rounds): # forbid zero padding, etc.
|
||||
raise uh.exc.MalformedHashError(cls)
|
||||
|
||||
# decode salt
|
||||
try:
|
||||
salt = ab64_decode(salt_str.encode("ascii"))
|
||||
except TypeError:
|
||||
raise uh.exc.MalformedHashError(cls)
|
||||
|
||||
# decode algs/digest list
|
||||
if not chk_str:
|
||||
# scram hashes MUST have something here.
|
||||
raise uh.exc.MalformedHashError(cls)
|
||||
elif "=" in chk_str:
|
||||
# comma-separated list of 'alg=digest' pairs
|
||||
algs = None
|
||||
chkmap = {}
|
||||
for pair in chk_str.split(","):
|
||||
alg, digest = pair.split("=")
|
||||
try:
|
||||
chkmap[alg] = ab64_decode(digest.encode("ascii"))
|
||||
except TypeError:
|
||||
raise uh.exc.MalformedHashError(cls)
|
||||
else:
|
||||
# comma-separated list of alg names, no digests
|
||||
algs = chk_str
|
||||
chkmap = None
|
||||
|
||||
# return new object
|
||||
return cls(
|
||||
rounds=rounds,
|
||||
salt=salt,
|
||||
checksum=chkmap,
|
||||
algs=algs,
|
||||
)
|
||||
|
||||
def to_string(self):
|
||||
salt = bascii_to_str(ab64_encode(self.salt))
|
||||
chkmap = self.checksum
|
||||
chk_str = ",".join(
|
||||
"%s=%s" % (alg, bascii_to_str(ab64_encode(chkmap[alg])))
|
||||
for alg in self.algs
|
||||
)
|
||||
return '$scram$%d$%s$%s' % (self.rounds, salt, chk_str)
|
||||
|
||||
#===================================================================
|
||||
# variant constructor
|
||||
#===================================================================
|
||||
@classmethod
|
||||
def using(cls, default_algs=None, algs=None, **kwds):
|
||||
# parse aliases
|
||||
if algs is not None:
|
||||
assert default_algs is None
|
||||
default_algs = algs
|
||||
|
||||
# create subclass
|
||||
subcls = super(scram, cls).using(**kwds)
|
||||
|
||||
# fill in algs
|
||||
if default_algs is not None:
|
||||
subcls.default_algs = cls._norm_algs(default_algs)
|
||||
return subcls
|
||||
|
||||
#===================================================================
|
||||
# init
|
||||
#===================================================================
|
||||
def __init__(self, algs=None, **kwds):
|
||||
super(scram, self).__init__(**kwds)
|
||||
|
||||
# init algs
|
||||
digest_map = self.checksum
|
||||
if algs is not None:
|
||||
if digest_map is not None:
|
||||
raise RuntimeError("checksum & algs kwds are mutually exclusive")
|
||||
algs = self._norm_algs(algs)
|
||||
elif digest_map is not None:
|
||||
# derive algs list from digest map (if present).
|
||||
algs = self._norm_algs(digest_map.keys())
|
||||
elif self.use_defaults:
|
||||
algs = list(self.default_algs)
|
||||
assert self._norm_algs(algs) == algs, "invalid default algs: %r" % (algs,)
|
||||
else:
|
||||
raise TypeError("no algs list specified")
|
||||
self.algs = algs
|
||||
|
||||
def _norm_checksum(self, checksum, relaxed=False):
|
||||
if not isinstance(checksum, dict):
|
||||
raise uh.exc.ExpectedTypeError(checksum, "dict", "checksum")
|
||||
for alg, digest in iteritems(checksum):
|
||||
if alg != norm_hash_name(alg, 'iana'):
|
||||
raise ValueError("malformed algorithm name in scram hash: %r" %
|
||||
(alg,))
|
||||
if len(alg) > 9:
|
||||
raise ValueError("SCRAM limits algorithm names to "
|
||||
"9 characters: %r" % (alg,))
|
||||
if not isinstance(digest, bytes):
|
||||
raise uh.exc.ExpectedTypeError(digest, "raw bytes", "digests")
|
||||
# TODO: verify digest size (if digest is known)
|
||||
if 'sha-1' not in checksum:
|
||||
# NOTE: required because of SCRAM spec.
|
||||
raise ValueError("sha-1 must be in algorithm list of scram hash")
|
||||
return checksum
|
||||
|
||||
@classmethod
|
||||
def _norm_algs(cls, algs):
|
||||
"""normalize algs parameter"""
|
||||
if isinstance(algs, native_string_types):
|
||||
algs = splitcomma(algs)
|
||||
algs = sorted(norm_hash_name(alg, 'iana') for alg in algs)
|
||||
if any(len(alg)>9 for alg in algs):
|
||||
raise ValueError("SCRAM limits alg names to max of 9 characters")
|
||||
if 'sha-1' not in algs:
|
||||
# NOTE: required because of SCRAM spec (rfc 5802)
|
||||
raise ValueError("sha-1 must be in algorithm list of scram hash")
|
||||
return algs
|
||||
|
||||
#===================================================================
|
||||
# migration
|
||||
#===================================================================
|
||||
def _calc_needs_update(self, **kwds):
|
||||
# marks hashes as deprecated if they don't include at least all default_algs.
|
||||
# XXX: should we deprecate if they aren't exactly the same,
|
||||
# to permit removing legacy hashes?
|
||||
if not set(self.algs).issuperset(self.default_algs):
|
||||
return True
|
||||
|
||||
# hand off to base implementation
|
||||
return super(scram, self)._calc_needs_update(**kwds)
|
||||
|
||||
#===================================================================
|
||||
# digest methods
|
||||
#===================================================================
|
||||
def _calc_checksum(self, secret, alg=None):
|
||||
rounds = self.rounds
|
||||
salt = self.salt
|
||||
hash = self.derive_digest
|
||||
if alg:
|
||||
# if requested, generate digest for specific alg
|
||||
return hash(secret, salt, rounds, alg)
|
||||
else:
|
||||
# by default, return dict containing digests for all algs
|
||||
return dict(
|
||||
(alg, hash(secret, salt, rounds, alg))
|
||||
for alg in self.algs
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def verify(cls, secret, hash, full=False):
|
||||
uh.validate_secret(secret)
|
||||
self = cls.from_string(hash)
|
||||
chkmap = self.checksum
|
||||
if not chkmap:
|
||||
raise ValueError("expected %s hash, got %s config string instead" %
|
||||
(cls.name, cls.name))
|
||||
|
||||
# NOTE: to make the verify method efficient, we just calculate hash
|
||||
# of shortest digest by default. apps can pass in "full=True" to
|
||||
# check entire hash for consistency.
|
||||
if full:
|
||||
correct = failed = False
|
||||
for alg, digest in iteritems(chkmap):
|
||||
other = self._calc_checksum(secret, alg)
|
||||
# NOTE: could do this length check in norm_algs(),
|
||||
# but don't need to be that strict, and want to be able
|
||||
# to parse hashes containing algs not supported by platform.
|
||||
# it's fine if we fail here though.
|
||||
if len(digest) != len(other):
|
||||
raise ValueError("mis-sized %s digest in scram hash: %r != %r"
|
||||
% (alg, len(digest), len(other)))
|
||||
if consteq(other, digest):
|
||||
correct = True
|
||||
else:
|
||||
failed = True
|
||||
if correct and failed:
|
||||
raise ValueError("scram hash verified inconsistently, "
|
||||
"may be corrupted")
|
||||
else:
|
||||
return correct
|
||||
else:
|
||||
# XXX: should this just always use sha1 hash? would be faster.
|
||||
# otherwise only verify against one hash, pick one w/ best security.
|
||||
for alg in self._verify_algs:
|
||||
if alg in chkmap:
|
||||
other = self._calc_checksum(secret, alg)
|
||||
return consteq(other, chkmap[alg])
|
||||
# there should always be sha-1 at the very least,
|
||||
# or something went wrong inside _norm_algs()
|
||||
raise AssertionError("sha-1 digest not found!")
|
||||
|
||||
#===================================================================
|
||||
#
|
||||
#===================================================================
|
||||
|
||||
#=============================================================================
|
||||
# code used for testing scram against protocol examples during development.
|
||||
#=============================================================================
|
||||
##def _test_reference_scram():
|
||||
## "quick hack testing scram reference vectors"
|
||||
## # NOTE: "n,," is GS2 header - see https://tools.ietf.org/html/rfc5801
|
||||
## from passlib.utils.compat import print_
|
||||
##
|
||||
## engine = _scram_engine(
|
||||
## alg="sha-1",
|
||||
## salt='QSXCR+Q6sek8bf92'.decode("base64"),
|
||||
## rounds=4096,
|
||||
## password=u("pencil"),
|
||||
## )
|
||||
## print_(engine.digest.encode("base64").rstrip())
|
||||
##
|
||||
## msg = engine.format_auth_msg(
|
||||
## username="user",
|
||||
## client_nonce = "fyko+d2lbbFgONRv9qkxdawL",
|
||||
## server_nonce = "3rfcNHYJY1ZVvWVs7j",
|
||||
## header='c=biws',
|
||||
## )
|
||||
##
|
||||
## cp = engine.get_encoded_client_proof(msg)
|
||||
## assert cp == "v0X8v3Bz2T0CJGbJQyF0X+HI4Ts=", cp
|
||||
##
|
||||
## ss = engine.get_encoded_server_sig(msg)
|
||||
## assert ss == "rmF9pqV8S7suAoZWja4dJRkFsKQ=", ss
|
||||
##
|
||||
##class _scram_engine(object):
|
||||
## """helper class for verifying scram hash behavior
|
||||
## against SCRAM protocol examples. not officially part of Passlib.
|
||||
##
|
||||
## takes in alg, salt, rounds, and a digest or password.
|
||||
##
|
||||
## can calculate the various keys & messages of the scram protocol.
|
||||
##
|
||||
## """
|
||||
## #=========================================================
|
||||
## # init
|
||||
## #=========================================================
|
||||
##
|
||||
## @classmethod
|
||||
## def from_string(cls, hash, alg):
|
||||
## "create record from scram hash, for given alg"
|
||||
## return cls(alg, *scram.extract_digest_info(hash, alg))
|
||||
##
|
||||
## def __init__(self, alg, salt, rounds, digest=None, password=None):
|
||||
## self.alg = norm_hash_name(alg)
|
||||
## self.salt = salt
|
||||
## self.rounds = rounds
|
||||
## self.password = password
|
||||
## if password:
|
||||
## data = scram.derive_digest(password, salt, rounds, alg)
|
||||
## if digest and data != digest:
|
||||
## raise ValueError("password doesn't match digest")
|
||||
## else:
|
||||
## digest = data
|
||||
## elif not digest:
|
||||
## raise TypeError("must provide password or digest")
|
||||
## self.digest = digest
|
||||
##
|
||||
## #=========================================================
|
||||
## # frontend methods
|
||||
## #=========================================================
|
||||
## def get_hash(self, data):
|
||||
## "return hash of raw data"
|
||||
## return hashlib.new(iana_to_hashlib(self.alg), data).digest()
|
||||
##
|
||||
## def get_client_proof(self, msg):
|
||||
## "return client proof of specified auth msg text"
|
||||
## return xor_bytes(self.client_key, self.get_client_sig(msg))
|
||||
##
|
||||
## def get_encoded_client_proof(self, msg):
|
||||
## return self.get_client_proof(msg).encode("base64").rstrip()
|
||||
##
|
||||
## def get_client_sig(self, msg):
|
||||
## "return client signature of specified auth msg text"
|
||||
## return self.get_hmac(self.stored_key, msg)
|
||||
##
|
||||
## def get_server_sig(self, msg):
|
||||
## "return server signature of specified auth msg text"
|
||||
## return self.get_hmac(self.server_key, msg)
|
||||
##
|
||||
## def get_encoded_server_sig(self, msg):
|
||||
## return self.get_server_sig(msg).encode("base64").rstrip()
|
||||
##
|
||||
## def format_server_response(self, client_nonce, server_nonce):
|
||||
## return 'r={client_nonce}{server_nonce},s={salt},i={rounds}'.format(
|
||||
## client_nonce=client_nonce,
|
||||
## server_nonce=server_nonce,
|
||||
## rounds=self.rounds,
|
||||
## salt=self.encoded_salt,
|
||||
## )
|
||||
##
|
||||
## def format_auth_msg(self, username, client_nonce, server_nonce,
|
||||
## header='c=biws'):
|
||||
## return (
|
||||
## 'n={username},r={client_nonce}'
|
||||
## ','
|
||||
## 'r={client_nonce}{server_nonce},s={salt},i={rounds}'
|
||||
## ','
|
||||
## '{header},r={client_nonce}{server_nonce}'
|
||||
## ).format(
|
||||
## username=username,
|
||||
## client_nonce=client_nonce,
|
||||
## server_nonce=server_nonce,
|
||||
## salt=self.encoded_salt,
|
||||
## rounds=self.rounds,
|
||||
## header=header,
|
||||
## )
|
||||
##
|
||||
## #=========================================================
|
||||
## # helpers to calculate & cache constant data
|
||||
## #=========================================================
|
||||
## def _calc_get_hmac(self):
|
||||
## return get_prf("hmac-" + iana_to_hashlib(self.alg))[0]
|
||||
##
|
||||
## def _calc_client_key(self):
|
||||
## return self.get_hmac(self.digest, b("Client Key"))
|
||||
##
|
||||
## def _calc_stored_key(self):
|
||||
## return self.get_hash(self.client_key)
|
||||
##
|
||||
## def _calc_server_key(self):
|
||||
## return self.get_hmac(self.digest, b("Server Key"))
|
||||
##
|
||||
## def _calc_encoded_salt(self):
|
||||
## return self.salt.encode("base64").rstrip()
|
||||
##
|
||||
## #=========================================================
|
||||
## # hacks for calculated attributes
|
||||
## #=========================================================
|
||||
##
|
||||
## def __getattr__(self, attr):
|
||||
## if not attr.startswith("_"):
|
||||
## f = getattr(self, "_calc_" + attr, None)
|
||||
## if f:
|
||||
## value = f()
|
||||
## setattr(self, attr, value)
|
||||
## return value
|
||||
## raise AttributeError("attribute not found")
|
||||
##
|
||||
## def __dir__(self):
|
||||
## cdir = dir(self.__class__)
|
||||
## attrs = set(cdir)
|
||||
## attrs.update(self.__dict__)
|
||||
## attrs.update(attr[6:] for attr in cdir
|
||||
## if attr.startswith("_calc_"))
|
||||
## return sorted(attrs)
|
||||
## #=========================================================
|
||||
## # eoc
|
||||
## #=========================================================
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
383
venv/lib/python3.12/site-packages/passlib/handlers/scrypt.py
Normal file
383
venv/lib/python3.12/site-packages/passlib/handlers/scrypt.py
Normal file
@@ -0,0 +1,383 @@
|
||||
"""passlib.handlers.scrypt -- scrypt password hash"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
from __future__ import with_statement, absolute_import
|
||||
# core
|
||||
import logging; log = logging.getLogger(__name__)
|
||||
# site
|
||||
# pkg
|
||||
from passlib.crypto import scrypt as _scrypt
|
||||
from passlib.utils import h64, to_bytes
|
||||
from passlib.utils.binary import h64, b64s_decode, b64s_encode
|
||||
from passlib.utils.compat import u, bascii_to_str, suppress_cause
|
||||
from passlib.utils.decor import classproperty
|
||||
import passlib.utils.handlers as uh
|
||||
# local
|
||||
__all__ = [
|
||||
"scrypt",
|
||||
]
|
||||
|
||||
#=============================================================================
|
||||
# scrypt format identifiers
|
||||
#=============================================================================
|
||||
|
||||
IDENT_SCRYPT = u("$scrypt$") # identifier used by passlib
|
||||
IDENT_7 = u("$7$") # used by official scrypt spec
|
||||
|
||||
_UDOLLAR = u("$")
|
||||
|
||||
#=============================================================================
|
||||
# handler
|
||||
#=============================================================================
|
||||
class scrypt(uh.ParallelismMixin, uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.HasManyIdents,
|
||||
uh.GenericHandler):
|
||||
"""This class implements an SCrypt-based password [#scrypt-home]_ hash, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports a variable-length salt, a variable number of rounds,
|
||||
as well as some custom tuning parameters unique to scrypt (see below).
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
|
||||
|
||||
:type salt: str
|
||||
:param salt:
|
||||
Optional salt string.
|
||||
If specified, the length must be between 0-1024 bytes.
|
||||
If not specified, one will be auto-generated (this is recommended).
|
||||
|
||||
:type salt_size: int
|
||||
:param salt_size:
|
||||
Optional number of bytes to use when autogenerating new salts.
|
||||
Defaults to 16 bytes, but can be any value between 0 and 1024.
|
||||
|
||||
:type rounds: int
|
||||
:param rounds:
|
||||
Optional number of rounds to use.
|
||||
Defaults to 16, but must be within ``range(1,32)``.
|
||||
|
||||
.. warning::
|
||||
|
||||
Unlike many hash algorithms, increasing the rounds value
|
||||
will increase both the time *and memory* required to hash a password.
|
||||
|
||||
:type block_size: int
|
||||
:param block_size:
|
||||
Optional block size to pass to scrypt hash function (the ``r`` parameter).
|
||||
Useful for tuning scrypt to optimal performance for your CPU architecture.
|
||||
Defaults to 8.
|
||||
|
||||
:type parallelism: int
|
||||
:param parallelism:
|
||||
Optional parallelism to pass to scrypt hash function (the ``p`` parameter).
|
||||
Defaults to 1.
|
||||
|
||||
:type relaxed: bool
|
||||
:param relaxed:
|
||||
By default, providing an invalid value for one of the other
|
||||
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
|
||||
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
|
||||
will be issued instead. Correctable errors include ``rounds``
|
||||
that are too small or too large, and ``salt`` strings that are too long.
|
||||
|
||||
.. note::
|
||||
|
||||
The underlying scrypt hash function has a number of limitations
|
||||
on it's parameter values, which forbids certain combinations of settings.
|
||||
The requirements are:
|
||||
|
||||
* ``linear_rounds = 2**<some positive integer>``
|
||||
* ``linear_rounds < 2**(16 * block_size)``
|
||||
* ``block_size * parallelism <= 2**30-1``
|
||||
|
||||
.. todo::
|
||||
|
||||
This class currently does not support configuring default values
|
||||
for ``block_size`` or ``parallelism`` via a :class:`~passlib.context.CryptContext`
|
||||
configuration.
|
||||
"""
|
||||
|
||||
#===================================================================
|
||||
# class attrs
|
||||
#===================================================================
|
||||
|
||||
#------------------------
|
||||
# PasswordHash
|
||||
#------------------------
|
||||
name = "scrypt"
|
||||
setting_kwds = ("ident", "salt", "salt_size", "rounds", "block_size", "parallelism")
|
||||
|
||||
#------------------------
|
||||
# GenericHandler
|
||||
#------------------------
|
||||
# NOTE: scrypt supports arbitrary output sizes. since it's output runs through
|
||||
# pbkdf2-hmac-sha256 before returning, and this could be raised eventually...
|
||||
# but a 256-bit digest is more than sufficient for password hashing.
|
||||
# XXX: make checksum size configurable? could merge w/ argon2 code that does this.
|
||||
checksum_size = 32
|
||||
|
||||
#------------------------
|
||||
# HasManyIdents
|
||||
#------------------------
|
||||
default_ident = IDENT_SCRYPT
|
||||
ident_values = (IDENT_SCRYPT, IDENT_7)
|
||||
|
||||
#------------------------
|
||||
# HasRawSalt
|
||||
#------------------------
|
||||
default_salt_size = 16
|
||||
max_salt_size = 1024
|
||||
|
||||
#------------------------
|
||||
# HasRounds
|
||||
#------------------------
|
||||
# TODO: would like to dynamically pick this based on system
|
||||
default_rounds = 16
|
||||
min_rounds = 1
|
||||
max_rounds = 31 # limited by scrypt alg
|
||||
rounds_cost = "log2"
|
||||
|
||||
# TODO: make default block size configurable via using(), and deprecatable via .needs_update()
|
||||
|
||||
#===================================================================
|
||||
# instance attrs
|
||||
#===================================================================
|
||||
|
||||
#: default parallelism setting (min=1 currently hardcoded in mixin)
|
||||
parallelism = 1
|
||||
|
||||
#: default block size setting
|
||||
block_size = 8
|
||||
|
||||
#===================================================================
|
||||
# variant constructor
|
||||
#===================================================================
|
||||
|
||||
@classmethod
|
||||
def using(cls, block_size=None, **kwds):
|
||||
subcls = super(scrypt, cls).using(**kwds)
|
||||
if block_size is not None:
|
||||
if isinstance(block_size, uh.native_string_types):
|
||||
block_size = int(block_size)
|
||||
subcls.block_size = subcls._norm_block_size(block_size, relaxed=kwds.get("relaxed"))
|
||||
|
||||
# make sure param combination is valid for scrypt()
|
||||
try:
|
||||
_scrypt.validate(1 << cls.default_rounds, cls.block_size, cls.parallelism)
|
||||
except ValueError as err:
|
||||
raise suppress_cause(ValueError("scrypt: invalid settings combination: " + str(err)))
|
||||
|
||||
return subcls
|
||||
|
||||
#===================================================================
|
||||
# parsing
|
||||
#===================================================================
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, hash):
|
||||
return cls(**cls.parse(hash))
|
||||
|
||||
@classmethod
|
||||
def parse(cls, hash):
|
||||
ident, suffix = cls._parse_ident(hash)
|
||||
func = getattr(cls, "_parse_%s_string" % ident.strip(_UDOLLAR), None)
|
||||
if func:
|
||||
return func(suffix)
|
||||
else:
|
||||
raise uh.exc.InvalidHashError(cls)
|
||||
|
||||
#
|
||||
# passlib's format:
|
||||
# $scrypt$ln=<logN>,r=<r>,p=<p>$<salt>[$<digest>]
|
||||
# where:
|
||||
# logN, r, p -- decimal-encoded positive integer, no zero-padding
|
||||
# logN -- log cost setting
|
||||
# r -- block size setting (usually 8)
|
||||
# p -- parallelism setting (usually 1)
|
||||
# salt, digest -- b64-nopad encoded bytes
|
||||
#
|
||||
|
||||
@classmethod
|
||||
def _parse_scrypt_string(cls, suffix):
|
||||
# break params, salt, and digest sections
|
||||
parts = suffix.split("$")
|
||||
if len(parts) == 3:
|
||||
params, salt, digest = parts
|
||||
elif len(parts) == 2:
|
||||
params, salt = parts
|
||||
digest = None
|
||||
else:
|
||||
raise uh.exc.MalformedHashError(cls, "malformed hash")
|
||||
|
||||
# break params apart
|
||||
parts = params.split(",")
|
||||
if len(parts) == 3:
|
||||
nstr, bstr, pstr = parts
|
||||
assert nstr.startswith("ln=")
|
||||
assert bstr.startswith("r=")
|
||||
assert pstr.startswith("p=")
|
||||
else:
|
||||
raise uh.exc.MalformedHashError(cls, "malformed settings field")
|
||||
|
||||
return dict(
|
||||
ident=IDENT_SCRYPT,
|
||||
rounds=int(nstr[3:]),
|
||||
block_size=int(bstr[2:]),
|
||||
parallelism=int(pstr[2:]),
|
||||
salt=b64s_decode(salt.encode("ascii")),
|
||||
checksum=b64s_decode(digest.encode("ascii")) if digest else None,
|
||||
)
|
||||
|
||||
#
|
||||
# official format specification defined at
|
||||
# https://gitlab.com/jas/scrypt-unix-crypt/blob/master/unix-scrypt.txt
|
||||
# format:
|
||||
# $7$<N><rrrrr><ppppp><salt...>[$<digest>]
|
||||
# 0 12345 67890 1
|
||||
# where:
|
||||
# All bytes use h64-little-endian encoding
|
||||
# N: 6-bit log cost setting
|
||||
# r: 30-bit block size setting
|
||||
# p: 30-bit parallelism setting
|
||||
# salt: variable length salt bytes
|
||||
# digest: fixed 32-byte digest
|
||||
#
|
||||
|
||||
@classmethod
|
||||
def _parse_7_string(cls, suffix):
|
||||
# XXX: annoyingly, official spec embeds salt *raw*, yet doesn't specify a hash encoding.
|
||||
# so assuming only h64 chars are valid for salt, and are ASCII encoded.
|
||||
|
||||
# split into params & digest
|
||||
parts = suffix.encode("ascii").split(b"$")
|
||||
if len(parts) == 2:
|
||||
params, digest = parts
|
||||
elif len(parts) == 1:
|
||||
params, = parts
|
||||
digest = None
|
||||
else:
|
||||
raise uh.exc.MalformedHashError()
|
||||
|
||||
# parse params & return
|
||||
if len(params) < 11:
|
||||
raise uh.exc.MalformedHashError(cls, "params field too short")
|
||||
return dict(
|
||||
ident=IDENT_7,
|
||||
rounds=h64.decode_int6(params[:1]),
|
||||
block_size=h64.decode_int30(params[1:6]),
|
||||
parallelism=h64.decode_int30(params[6:11]),
|
||||
salt=params[11:],
|
||||
checksum=h64.decode_bytes(digest) if digest else None,
|
||||
)
|
||||
|
||||
#===================================================================
|
||||
# formatting
|
||||
#===================================================================
|
||||
def to_string(self):
|
||||
ident = self.ident
|
||||
if ident == IDENT_SCRYPT:
|
||||
return "$scrypt$ln=%d,r=%d,p=%d$%s$%s" % (
|
||||
self.rounds,
|
||||
self.block_size,
|
||||
self.parallelism,
|
||||
bascii_to_str(b64s_encode(self.salt)),
|
||||
bascii_to_str(b64s_encode(self.checksum)),
|
||||
)
|
||||
else:
|
||||
assert ident == IDENT_7
|
||||
salt = self.salt
|
||||
try:
|
||||
salt.decode("ascii")
|
||||
except UnicodeDecodeError:
|
||||
raise suppress_cause(NotImplementedError("scrypt $7$ hashes dont support non-ascii salts"))
|
||||
return bascii_to_str(b"".join([
|
||||
b"$7$",
|
||||
h64.encode_int6(self.rounds),
|
||||
h64.encode_int30(self.block_size),
|
||||
h64.encode_int30(self.parallelism),
|
||||
self.salt,
|
||||
b"$",
|
||||
h64.encode_bytes(self.checksum)
|
||||
]))
|
||||
|
||||
#===================================================================
|
||||
# init
|
||||
#===================================================================
|
||||
def __init__(self, block_size=None, **kwds):
|
||||
super(scrypt, self).__init__(**kwds)
|
||||
|
||||
# init block size
|
||||
if block_size is None:
|
||||
assert uh.validate_default_value(self, self.block_size, self._norm_block_size,
|
||||
param="block_size")
|
||||
else:
|
||||
self.block_size = self._norm_block_size(block_size)
|
||||
|
||||
# NOTE: if hash contains invalid complex constraint, relying on error
|
||||
# being raised by scrypt call in _calc_checksum()
|
||||
|
||||
@classmethod
|
||||
def _norm_block_size(cls, block_size, relaxed=False):
|
||||
return uh.norm_integer(cls, block_size, min=1, param="block_size", relaxed=relaxed)
|
||||
|
||||
def _generate_salt(self):
|
||||
salt = super(scrypt, self)._generate_salt()
|
||||
if self.ident == IDENT_7:
|
||||
# this format doesn't support non-ascii salts.
|
||||
# as workaround, we take raw bytes, encoded to base64
|
||||
salt = b64s_encode(salt)
|
||||
return salt
|
||||
|
||||
#===================================================================
|
||||
# backend configuration
|
||||
# NOTE: this following HasManyBackends' API, but provides it's own implementation,
|
||||
# which actually switches the backend that 'passlib.crypto.scrypt.scrypt()' uses.
|
||||
#===================================================================
|
||||
|
||||
@classproperty
|
||||
def backends(cls):
|
||||
return _scrypt.backend_values
|
||||
|
||||
@classmethod
|
||||
def get_backend(cls):
|
||||
return _scrypt.backend
|
||||
|
||||
@classmethod
|
||||
def has_backend(cls, name="any"):
|
||||
try:
|
||||
cls.set_backend(name, dryrun=True)
|
||||
return True
|
||||
except uh.exc.MissingBackendError:
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def set_backend(cls, name="any", dryrun=False):
|
||||
_scrypt._set_backend(name, dryrun=dryrun)
|
||||
|
||||
#===================================================================
|
||||
# digest calculation
|
||||
#===================================================================
|
||||
def _calc_checksum(self, secret):
|
||||
secret = to_bytes(secret, param="secret")
|
||||
return _scrypt.scrypt(secret, self.salt, n=(1 << self.rounds), r=self.block_size,
|
||||
p=self.parallelism, keylen=self.checksum_size)
|
||||
|
||||
#===================================================================
|
||||
# hash migration
|
||||
#===================================================================
|
||||
|
||||
def _calc_needs_update(self, **kwds):
|
||||
"""
|
||||
mark hash as needing update if rounds is outside desired bounds.
|
||||
"""
|
||||
# XXX: for now, marking all hashes which don't have matching block_size setting
|
||||
if self.block_size != type(self).block_size:
|
||||
return True
|
||||
return super(scrypt, self)._calc_needs_update(**kwds)
|
||||
|
||||
#===================================================================
|
||||
# eoc
|
||||
#===================================================================
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
158
venv/lib/python3.12/site-packages/passlib/handlers/sha1_crypt.py
Normal file
158
venv/lib/python3.12/site-packages/passlib/handlers/sha1_crypt.py
Normal file
@@ -0,0 +1,158 @@
|
||||
"""passlib.handlers.sha1_crypt
|
||||
"""
|
||||
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
|
||||
# core
|
||||
import logging; log = logging.getLogger(__name__)
|
||||
# site
|
||||
# pkg
|
||||
from passlib.utils import safe_crypt, test_crypt
|
||||
from passlib.utils.binary import h64
|
||||
from passlib.utils.compat import u, unicode, irange
|
||||
from passlib.crypto.digest import compile_hmac
|
||||
import passlib.utils.handlers as uh
|
||||
# local
|
||||
__all__ = [
|
||||
]
|
||||
#=============================================================================
|
||||
# sha1-crypt
|
||||
#=============================================================================
|
||||
_BNULL = b'\x00'
|
||||
|
||||
class sha1_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler):
|
||||
"""This class implements the SHA1-Crypt password hash, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports a variable-length salt, and a variable number of rounds.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
|
||||
|
||||
:type salt: str
|
||||
:param salt:
|
||||
Optional salt string.
|
||||
If not specified, an 8 character one will be autogenerated (this is recommended).
|
||||
If specified, it must be 0-64 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
|
||||
|
||||
:type salt_size: int
|
||||
:param salt_size:
|
||||
Optional number of bytes to use when autogenerating new salts.
|
||||
Defaults to 8 bytes, but can be any value between 0 and 64.
|
||||
|
||||
:type rounds: int
|
||||
:param rounds:
|
||||
Optional number of rounds to use.
|
||||
Defaults to 480000, must be between 1 and 4294967295, inclusive.
|
||||
|
||||
:type relaxed: bool
|
||||
:param relaxed:
|
||||
By default, providing an invalid value for one of the other
|
||||
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
|
||||
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
|
||||
will be issued instead. Correctable errors include ``rounds``
|
||||
that are too small or too large, and ``salt`` strings that are too long.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
"""
|
||||
|
||||
#===================================================================
|
||||
# class attrs
|
||||
#===================================================================
|
||||
#--GenericHandler--
|
||||
name = "sha1_crypt"
|
||||
setting_kwds = ("salt", "salt_size", "rounds")
|
||||
ident = u("$sha1$")
|
||||
checksum_size = 28
|
||||
checksum_chars = uh.HASH64_CHARS
|
||||
|
||||
#--HasSalt--
|
||||
default_salt_size = 8
|
||||
max_salt_size = 64
|
||||
salt_chars = uh.HASH64_CHARS
|
||||
|
||||
#--HasRounds--
|
||||
default_rounds = 480000 # current passlib default
|
||||
min_rounds = 1 # really, this should be higher.
|
||||
max_rounds = 4294967295 # 32-bit integer limit
|
||||
rounds_cost = "linear"
|
||||
|
||||
#===================================================================
|
||||
# formatting
|
||||
#===================================================================
|
||||
@classmethod
|
||||
def from_string(cls, hash):
|
||||
rounds, salt, chk = uh.parse_mc3(hash, cls.ident, handler=cls)
|
||||
return cls(rounds=rounds, salt=salt, checksum=chk)
|
||||
|
||||
def to_string(self, config=False):
|
||||
chk = None if config else self.checksum
|
||||
return uh.render_mc3(self.ident, self.rounds, self.salt, chk)
|
||||
|
||||
#===================================================================
|
||||
# backend
|
||||
#===================================================================
|
||||
backends = ("os_crypt", "builtin")
|
||||
|
||||
#---------------------------------------------------------------
|
||||
# os_crypt backend
|
||||
#---------------------------------------------------------------
|
||||
@classmethod
|
||||
def _load_backend_os_crypt(cls):
|
||||
if test_crypt("test", '$sha1$1$Wq3GL2Vp$C8U25GvfHS8qGHim'
|
||||
'ExLaiSFlGkAe'):
|
||||
cls._set_calc_checksum_backend(cls._calc_checksum_os_crypt)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def _calc_checksum_os_crypt(self, secret):
|
||||
config = self.to_string(config=True)
|
||||
hash = safe_crypt(secret, config)
|
||||
if hash is None:
|
||||
# py3's crypt.crypt() can't handle non-utf8 bytes.
|
||||
# fallback to builtin alg, which is always available.
|
||||
return self._calc_checksum_builtin(secret)
|
||||
if not hash.startswith(config) or len(hash) != len(config) + 29:
|
||||
raise uh.exc.CryptBackendError(self, config, hash)
|
||||
return hash[-28:]
|
||||
|
||||
#---------------------------------------------------------------
|
||||
# builtin backend
|
||||
#---------------------------------------------------------------
|
||||
@classmethod
|
||||
def _load_backend_builtin(cls):
|
||||
cls._set_calc_checksum_backend(cls._calc_checksum_builtin)
|
||||
return True
|
||||
|
||||
def _calc_checksum_builtin(self, secret):
|
||||
if isinstance(secret, unicode):
|
||||
secret = secret.encode("utf-8")
|
||||
if _BNULL in secret:
|
||||
raise uh.exc.NullPasswordError(self)
|
||||
rounds = self.rounds
|
||||
# NOTE: this seed value is NOT the same as the config string
|
||||
result = (u("%s$sha1$%s") % (self.salt, rounds)).encode("ascii")
|
||||
# NOTE: this algorithm is essentially PBKDF1, modified to use HMAC.
|
||||
keyed_hmac = compile_hmac("sha1", secret)
|
||||
for _ in irange(rounds):
|
||||
result = keyed_hmac(result)
|
||||
return h64.encode_transposed_bytes(result, self._chk_offsets).decode("ascii")
|
||||
|
||||
_chk_offsets = [
|
||||
2,1,0,
|
||||
5,4,3,
|
||||
8,7,6,
|
||||
11,10,9,
|
||||
14,13,12,
|
||||
17,16,15,
|
||||
0,19,18,
|
||||
]
|
||||
|
||||
#===================================================================
|
||||
# eoc
|
||||
#===================================================================
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
534
venv/lib/python3.12/site-packages/passlib/handlers/sha2_crypt.py
Normal file
534
venv/lib/python3.12/site-packages/passlib/handlers/sha2_crypt.py
Normal file
@@ -0,0 +1,534 @@
|
||||
"""passlib.handlers.sha2_crypt - SHA256-Crypt / SHA512-Crypt"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
# core
|
||||
import hashlib
|
||||
import logging; log = logging.getLogger(__name__)
|
||||
# site
|
||||
# pkg
|
||||
from passlib.utils import safe_crypt, test_crypt, \
|
||||
repeat_string, to_unicode
|
||||
from passlib.utils.binary import h64
|
||||
from passlib.utils.compat import byte_elem_value, u, \
|
||||
uascii_to_str, unicode
|
||||
import passlib.utils.handlers as uh
|
||||
# local
|
||||
__all__ = [
|
||||
"sha512_crypt",
|
||||
"sha256_crypt",
|
||||
]
|
||||
|
||||
#=============================================================================
|
||||
# pure-python backend, used by both sha256_crypt & sha512_crypt
|
||||
# when crypt.crypt() backend is not available.
|
||||
#=============================================================================
|
||||
_BNULL = b'\x00'
|
||||
|
||||
# pre-calculated offsets used to speed up C digest stage (see notes below).
|
||||
# sequence generated using the following:
|
||||
##perms_order = "p,pp,ps,psp,sp,spp".split(",")
|
||||
##def offset(i):
|
||||
## key = (("p" if i % 2 else "") + ("s" if i % 3 else "") +
|
||||
## ("p" if i % 7 else "") + ("" if i % 2 else "p"))
|
||||
## return perms_order.index(key)
|
||||
##_c_digest_offsets = [(offset(i), offset(i+1)) for i in range(0,42,2)]
|
||||
_c_digest_offsets = (
|
||||
(0, 3), (5, 1), (5, 3), (1, 2), (5, 1), (5, 3), (1, 3),
|
||||
(4, 1), (5, 3), (1, 3), (5, 0), (5, 3), (1, 3), (5, 1),
|
||||
(4, 3), (1, 3), (5, 1), (5, 2), (1, 3), (5, 1), (5, 3),
|
||||
)
|
||||
|
||||
# map used to transpose bytes when encoding final sha256_crypt digest
|
||||
_256_transpose_map = (
|
||||
20, 10, 0, 11, 1, 21, 2, 22, 12, 23, 13, 3, 14, 4, 24, 5,
|
||||
25, 15, 26, 16, 6, 17, 7, 27, 8, 28, 18, 29, 19, 9, 30, 31,
|
||||
)
|
||||
|
||||
# map used to transpose bytes when encoding final sha512_crypt digest
|
||||
_512_transpose_map = (
|
||||
42, 21, 0, 1, 43, 22, 23, 2, 44, 45, 24, 3, 4, 46, 25, 26,
|
||||
5, 47, 48, 27, 6, 7, 49, 28, 29, 8, 50, 51, 30, 9, 10, 52,
|
||||
31, 32, 11, 53, 54, 33, 12, 13, 55, 34, 35, 14, 56, 57, 36, 15,
|
||||
16, 58, 37, 38, 17, 59, 60, 39, 18, 19, 61, 40, 41, 20, 62, 63,
|
||||
)
|
||||
|
||||
def _raw_sha2_crypt(pwd, salt, rounds, use_512=False):
|
||||
"""perform raw sha256-crypt / sha512-crypt
|
||||
|
||||
this function provides a pure-python implementation of the internals
|
||||
for the SHA256-Crypt and SHA512-Crypt algorithms; it doesn't
|
||||
handle any of the parsing/validation of the hash strings themselves.
|
||||
|
||||
:arg pwd: password chars/bytes to hash
|
||||
:arg salt: salt chars to use
|
||||
:arg rounds: linear rounds cost
|
||||
:arg use_512: use sha512-crypt instead of sha256-crypt mode
|
||||
|
||||
:returns:
|
||||
encoded checksum chars
|
||||
"""
|
||||
#===================================================================
|
||||
# init & validate inputs
|
||||
#===================================================================
|
||||
|
||||
# NOTE: the setup portion of this algorithm scales ~linearly in time
|
||||
# with the size of the password, making it vulnerable to a DOS from
|
||||
# unreasonably large inputs. the following code has some optimizations
|
||||
# which would make things even worse, using O(pwd_len**2) memory
|
||||
# when calculating digest P.
|
||||
#
|
||||
# to mitigate these two issues: 1) this code switches to a
|
||||
# O(pwd_len)-memory algorithm for passwords that are much larger
|
||||
# than average, and 2) Passlib enforces a library-wide max limit on
|
||||
# the size of passwords it will allow, to prevent this algorithm and
|
||||
# others from being DOSed in this way (see passlib.exc.PasswordSizeError
|
||||
# for details).
|
||||
|
||||
# validate secret
|
||||
if isinstance(pwd, unicode):
|
||||
# XXX: not sure what official unicode policy is, using this as default
|
||||
pwd = pwd.encode("utf-8")
|
||||
assert isinstance(pwd, bytes)
|
||||
if _BNULL in pwd:
|
||||
raise uh.exc.NullPasswordError(sha512_crypt if use_512 else sha256_crypt)
|
||||
pwd_len = len(pwd)
|
||||
|
||||
# validate rounds
|
||||
assert 1000 <= rounds <= 999999999, "invalid rounds"
|
||||
# NOTE: spec says out-of-range rounds should be clipped, instead of
|
||||
# causing an error. this function assumes that's been taken care of
|
||||
# by the handler class.
|
||||
|
||||
# validate salt
|
||||
assert isinstance(salt, unicode), "salt not unicode"
|
||||
salt = salt.encode("ascii")
|
||||
salt_len = len(salt)
|
||||
assert salt_len < 17, "salt too large"
|
||||
# NOTE: spec says salts larger than 16 bytes should be truncated,
|
||||
# instead of causing an error. this function assumes that's been
|
||||
# taken care of by the handler class.
|
||||
|
||||
# load sha256/512 specific constants
|
||||
if use_512:
|
||||
hash_const = hashlib.sha512
|
||||
transpose_map = _512_transpose_map
|
||||
else:
|
||||
hash_const = hashlib.sha256
|
||||
transpose_map = _256_transpose_map
|
||||
|
||||
#===================================================================
|
||||
# digest B - used as subinput to digest A
|
||||
#===================================================================
|
||||
db = hash_const(pwd + salt + pwd).digest()
|
||||
|
||||
#===================================================================
|
||||
# digest A - used to initialize first round of digest C
|
||||
#===================================================================
|
||||
# start out with pwd + salt
|
||||
a_ctx = hash_const(pwd + salt)
|
||||
a_ctx_update = a_ctx.update
|
||||
|
||||
# add pwd_len bytes of b, repeating b as many times as needed.
|
||||
a_ctx_update(repeat_string(db, pwd_len))
|
||||
|
||||
# for each bit in pwd_len: add b if it's 1, or pwd if it's 0
|
||||
i = pwd_len
|
||||
while i:
|
||||
a_ctx_update(db if i & 1 else pwd)
|
||||
i >>= 1
|
||||
|
||||
# finish A
|
||||
da = a_ctx.digest()
|
||||
|
||||
#===================================================================
|
||||
# digest P from password - used instead of password itself
|
||||
# when calculating digest C.
|
||||
#===================================================================
|
||||
if pwd_len < 96:
|
||||
# this method is faster under python, but uses O(pwd_len**2) memory;
|
||||
# so we don't use it for larger passwords to avoid a potential DOS.
|
||||
dp = repeat_string(hash_const(pwd * pwd_len).digest(), pwd_len)
|
||||
else:
|
||||
# this method is slower under python, but uses a fixed amount of memory.
|
||||
tmp_ctx = hash_const(pwd)
|
||||
tmp_ctx_update = tmp_ctx.update
|
||||
i = pwd_len-1
|
||||
while i:
|
||||
tmp_ctx_update(pwd)
|
||||
i -= 1
|
||||
dp = repeat_string(tmp_ctx.digest(), pwd_len)
|
||||
assert len(dp) == pwd_len
|
||||
|
||||
#===================================================================
|
||||
# digest S - used instead of salt itself when calculating digest C
|
||||
#===================================================================
|
||||
ds = hash_const(salt * (16 + byte_elem_value(da[0]))).digest()[:salt_len]
|
||||
assert len(ds) == salt_len, "salt_len somehow > hash_len!"
|
||||
|
||||
#===================================================================
|
||||
# digest C - for a variable number of rounds, combine A, S, and P
|
||||
# digests in various ways; in order to burn CPU time.
|
||||
#===================================================================
|
||||
|
||||
# NOTE: the original SHA256/512-Crypt specification performs the C digest
|
||||
# calculation using the following loop:
|
||||
#
|
||||
##dc = da
|
||||
##i = 0
|
||||
##while i < rounds:
|
||||
## tmp_ctx = hash_const(dp if i & 1 else dc)
|
||||
## if i % 3:
|
||||
## tmp_ctx.update(ds)
|
||||
## if i % 7:
|
||||
## tmp_ctx.update(dp)
|
||||
## tmp_ctx.update(dc if i & 1 else dp)
|
||||
## dc = tmp_ctx.digest()
|
||||
## i += 1
|
||||
#
|
||||
# The code Passlib uses (below) implements an equivalent algorithm,
|
||||
# it's just been heavily optimized to pre-calculate a large number
|
||||
# of things beforehand. It works off of a couple of observations
|
||||
# about the original algorithm:
|
||||
#
|
||||
# 1. each round is a combination of 'dc', 'ds', and 'dp'; determined
|
||||
# by the whether 'i' a multiple of 2,3, and/or 7.
|
||||
# 2. since lcm(2,3,7)==42, the series of combinations will repeat
|
||||
# every 42 rounds.
|
||||
# 3. even rounds 0-40 consist of 'hash(dc + round-specific-constant)';
|
||||
# while odd rounds 1-41 consist of hash(round-specific-constant + dc)
|
||||
#
|
||||
# Using these observations, the following code...
|
||||
# * calculates the round-specific combination of ds & dp for each round 0-41
|
||||
# * runs through as many 42-round blocks as possible
|
||||
# * runs through as many pairs of rounds as possible for remaining rounds
|
||||
# * performs once last round if the total rounds should be odd.
|
||||
#
|
||||
# this cuts out a lot of the control overhead incurred when running the
|
||||
# original loop 40,000+ times in python, resulting in ~20% increase in
|
||||
# speed under CPython (though still 2x slower than glibc crypt)
|
||||
|
||||
# prepare the 6 combinations of ds & dp which are needed
|
||||
# (order of 'perms' must match how _c_digest_offsets was generated)
|
||||
dp_dp = dp+dp
|
||||
dp_ds = dp+ds
|
||||
perms = [dp, dp_dp, dp_ds, dp_ds+dp, ds+dp, ds+dp_dp]
|
||||
|
||||
# build up list of even-round & odd-round constants,
|
||||
# and store in 21-element list as (even,odd) pairs.
|
||||
data = [ (perms[even], perms[odd]) for even, odd in _c_digest_offsets]
|
||||
|
||||
# perform as many full 42-round blocks as possible
|
||||
dc = da
|
||||
blocks, tail = divmod(rounds, 42)
|
||||
while blocks:
|
||||
for even, odd in data:
|
||||
dc = hash_const(odd + hash_const(dc + even).digest()).digest()
|
||||
blocks -= 1
|
||||
|
||||
# perform any leftover rounds
|
||||
if tail:
|
||||
# perform any pairs of rounds
|
||||
pairs = tail>>1
|
||||
for even, odd in data[:pairs]:
|
||||
dc = hash_const(odd + hash_const(dc + even).digest()).digest()
|
||||
|
||||
# if rounds was odd, do one last round (since we started at 0,
|
||||
# last round will be an even-numbered round)
|
||||
if tail & 1:
|
||||
dc = hash_const(dc + data[pairs][0]).digest()
|
||||
|
||||
#===================================================================
|
||||
# encode digest using appropriate transpose map
|
||||
#===================================================================
|
||||
return h64.encode_transposed_bytes(dc, transpose_map).decode("ascii")
|
||||
|
||||
#=============================================================================
|
||||
# handlers
|
||||
#=============================================================================
|
||||
_UROUNDS = u("rounds=")
|
||||
_UDOLLAR = u("$")
|
||||
_UZERO = u("0")
|
||||
|
||||
class _SHA2_Common(uh.HasManyBackends, uh.HasRounds, uh.HasSalt,
|
||||
uh.GenericHandler):
|
||||
"""class containing common code shared by sha256_crypt & sha512_crypt"""
|
||||
#===================================================================
|
||||
# class attrs
|
||||
#===================================================================
|
||||
# name - set by subclass
|
||||
setting_kwds = ("salt", "rounds", "implicit_rounds", "salt_size")
|
||||
# ident - set by subclass
|
||||
checksum_chars = uh.HASH64_CHARS
|
||||
# checksum_size - set by subclass
|
||||
|
||||
max_salt_size = 16
|
||||
salt_chars = uh.HASH64_CHARS
|
||||
|
||||
min_rounds = 1000 # bounds set by spec
|
||||
max_rounds = 999999999 # bounds set by spec
|
||||
rounds_cost = "linear"
|
||||
|
||||
_cdb_use_512 = False # flag for _calc_digest_builtin()
|
||||
_rounds_prefix = None # ident + _UROUNDS
|
||||
|
||||
#===================================================================
|
||||
# methods
|
||||
#===================================================================
|
||||
implicit_rounds = False
|
||||
|
||||
def __init__(self, implicit_rounds=None, **kwds):
|
||||
super(_SHA2_Common, self).__init__(**kwds)
|
||||
# if user calls hash() w/ 5000 rounds, default to compact form.
|
||||
if implicit_rounds is None:
|
||||
implicit_rounds = (self.use_defaults and self.rounds == 5000)
|
||||
self.implicit_rounds = implicit_rounds
|
||||
|
||||
def _parse_salt(self, salt):
|
||||
# required per SHA2-crypt spec -- truncate config salts rather than throwing error
|
||||
return self._norm_salt(salt, relaxed=self.checksum is None)
|
||||
|
||||
def _parse_rounds(self, rounds):
|
||||
# required per SHA2-crypt spec -- clip config rounds rather than throwing error
|
||||
return self._norm_rounds(rounds, relaxed=self.checksum is None)
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, hash):
|
||||
# basic format this parses -
|
||||
# $5$[rounds=<rounds>$]<salt>[$<checksum>]
|
||||
|
||||
# TODO: this *could* use uh.parse_mc3(), except that the rounds
|
||||
# portion has a slightly different grammar.
|
||||
|
||||
# convert to unicode, check for ident prefix, split on dollar signs.
|
||||
hash = to_unicode(hash, "ascii", "hash")
|
||||
ident = cls.ident
|
||||
if not hash.startswith(ident):
|
||||
raise uh.exc.InvalidHashError(cls)
|
||||
assert len(ident) == 3
|
||||
parts = hash[3:].split(_UDOLLAR)
|
||||
|
||||
# extract rounds value
|
||||
if parts[0].startswith(_UROUNDS):
|
||||
assert len(_UROUNDS) == 7
|
||||
rounds = parts.pop(0)[7:]
|
||||
if rounds.startswith(_UZERO) and rounds != _UZERO:
|
||||
raise uh.exc.ZeroPaddedRoundsError(cls)
|
||||
rounds = int(rounds)
|
||||
implicit_rounds = False
|
||||
else:
|
||||
rounds = 5000
|
||||
implicit_rounds = True
|
||||
|
||||
# rest should be salt and checksum
|
||||
if len(parts) == 2:
|
||||
salt, chk = parts
|
||||
elif len(parts) == 1:
|
||||
salt = parts[0]
|
||||
chk = None
|
||||
else:
|
||||
raise uh.exc.MalformedHashError(cls)
|
||||
|
||||
# return new object
|
||||
return cls(
|
||||
rounds=rounds,
|
||||
salt=salt,
|
||||
checksum=chk or None,
|
||||
implicit_rounds=implicit_rounds,
|
||||
)
|
||||
|
||||
def to_string(self):
|
||||
if self.rounds == 5000 and self.implicit_rounds:
|
||||
hash = u("%s%s$%s") % (self.ident, self.salt,
|
||||
self.checksum or u(''))
|
||||
else:
|
||||
hash = u("%srounds=%d$%s$%s") % (self.ident, self.rounds,
|
||||
self.salt, self.checksum or u(''))
|
||||
return uascii_to_str(hash)
|
||||
|
||||
#===================================================================
|
||||
# backends
|
||||
#===================================================================
|
||||
backends = ("os_crypt", "builtin")
|
||||
|
||||
#---------------------------------------------------------------
|
||||
# os_crypt backend
|
||||
#---------------------------------------------------------------
|
||||
|
||||
#: test hash for OS detection -- provided by subclass
|
||||
_test_hash = None
|
||||
|
||||
@classmethod
|
||||
def _load_backend_os_crypt(cls):
|
||||
if test_crypt(*cls._test_hash):
|
||||
cls._set_calc_checksum_backend(cls._calc_checksum_os_crypt)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def _calc_checksum_os_crypt(self, secret):
|
||||
config = self.to_string()
|
||||
hash = safe_crypt(secret, config)
|
||||
if hash is None:
|
||||
# py3's crypt.crypt() can't handle non-utf8 bytes.
|
||||
# fallback to builtin alg, which is always available.
|
||||
return self._calc_checksum_builtin(secret)
|
||||
# NOTE: avoiding full parsing routine via from_string().checksum,
|
||||
# and just extracting the bit we need.
|
||||
cs = self.checksum_size
|
||||
if not hash.startswith(self.ident) or hash[-cs-1] != _UDOLLAR:
|
||||
raise uh.exc.CryptBackendError(self, config, hash)
|
||||
return hash[-cs:]
|
||||
|
||||
#---------------------------------------------------------------
|
||||
# builtin backend
|
||||
#---------------------------------------------------------------
|
||||
@classmethod
|
||||
def _load_backend_builtin(cls):
|
||||
cls._set_calc_checksum_backend(cls._calc_checksum_builtin)
|
||||
return True
|
||||
|
||||
def _calc_checksum_builtin(self, secret):
|
||||
return _raw_sha2_crypt(secret, self.salt, self.rounds,
|
||||
self._cdb_use_512)
|
||||
|
||||
#===================================================================
|
||||
# eoc
|
||||
#===================================================================
|
||||
|
||||
class sha256_crypt(_SHA2_Common):
|
||||
"""This class implements the SHA256-Crypt password hash, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports a variable-length salt, and a variable number of rounds.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
|
||||
|
||||
:type salt: str
|
||||
:param salt:
|
||||
Optional salt string.
|
||||
If not specified, one will be autogenerated (this is recommended).
|
||||
If specified, it must be 0-16 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
|
||||
|
||||
:type rounds: int
|
||||
:param rounds:
|
||||
Optional number of rounds to use.
|
||||
Defaults to 535000, must be between 1000 and 999999999, inclusive.
|
||||
|
||||
.. note::
|
||||
per the official specification, when the rounds parameter is set to 5000,
|
||||
it may be omitted from the hash string.
|
||||
|
||||
:type relaxed: bool
|
||||
:param relaxed:
|
||||
By default, providing an invalid value for one of the other
|
||||
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
|
||||
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
|
||||
will be issued instead. Correctable errors include ``rounds``
|
||||
that are too small or too large, and ``salt`` strings that are too long.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
|
||||
..
|
||||
commented out, currently only supported by :meth:`hash`, and not via :meth:`using`:
|
||||
|
||||
:type implicit_rounds: bool
|
||||
:param implicit_rounds:
|
||||
this is an internal option which generally doesn't need to be touched.
|
||||
|
||||
this flag determines whether the hash should omit the rounds parameter
|
||||
when encoding it to a string; this is only permitted by the spec for rounds=5000,
|
||||
and the flag is ignored otherwise. the spec requires the two different
|
||||
encodings be preserved as they are, instead of normalizing them.
|
||||
"""
|
||||
#===================================================================
|
||||
# class attrs
|
||||
#===================================================================
|
||||
name = "sha256_crypt"
|
||||
ident = u("$5$")
|
||||
checksum_size = 43
|
||||
# NOTE: using 25/75 weighting of builtin & os_crypt backends
|
||||
default_rounds = 535000
|
||||
|
||||
#===================================================================
|
||||
# backends
|
||||
#===================================================================
|
||||
_test_hash = ("test", "$5$rounds=1000$test$QmQADEXMG8POI5W"
|
||||
"Dsaeho0P36yK3Tcrgboabng6bkb/")
|
||||
|
||||
#===================================================================
|
||||
# eoc
|
||||
#===================================================================
|
||||
|
||||
#=============================================================================
|
||||
# sha 512 crypt
|
||||
#=============================================================================
|
||||
class sha512_crypt(_SHA2_Common):
|
||||
"""This class implements the SHA512-Crypt password hash, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports a variable-length salt, and a variable number of rounds.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
|
||||
|
||||
:type salt: str
|
||||
:param salt:
|
||||
Optional salt string.
|
||||
If not specified, one will be autogenerated (this is recommended).
|
||||
If specified, it must be 0-16 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
|
||||
|
||||
:type rounds: int
|
||||
:param rounds:
|
||||
Optional number of rounds to use.
|
||||
Defaults to 656000, must be between 1000 and 999999999, inclusive.
|
||||
|
||||
.. note::
|
||||
per the official specification, when the rounds parameter is set to 5000,
|
||||
it may be omitted from the hash string.
|
||||
|
||||
:type relaxed: bool
|
||||
:param relaxed:
|
||||
By default, providing an invalid value for one of the other
|
||||
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
|
||||
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
|
||||
will be issued instead. Correctable errors include ``rounds``
|
||||
that are too small or too large, and ``salt`` strings that are too long.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
|
||||
..
|
||||
commented out, currently only supported by :meth:`hash`, and not via :meth:`using`:
|
||||
|
||||
:type implicit_rounds: bool
|
||||
:param implicit_rounds:
|
||||
this is an internal option which generally doesn't need to be touched.
|
||||
|
||||
this flag determines whether the hash should omit the rounds parameter
|
||||
when encoding it to a string; this is only permitted by the spec for rounds=5000,
|
||||
and the flag is ignored otherwise. the spec requires the two different
|
||||
encodings be preserved as they are, instead of normalizing them.
|
||||
"""
|
||||
|
||||
#===================================================================
|
||||
# class attrs
|
||||
#===================================================================
|
||||
name = "sha512_crypt"
|
||||
ident = u("$6$")
|
||||
checksum_size = 86
|
||||
_cdb_use_512 = True
|
||||
# NOTE: using 25/75 weighting of builtin & os_crypt backends
|
||||
default_rounds = 656000
|
||||
|
||||
#===================================================================
|
||||
# backend
|
||||
#===================================================================
|
||||
_test_hash = ("test", "$6$rounds=1000$test$2M/Lx6Mtobqj"
|
||||
"Ljobw0Wmo4Q5OFx5nVLJvmgseatA6oMn"
|
||||
"yWeBdRDx4DU.1H3eGmse6pgsOgDisWBG"
|
||||
"I5c7TZauS0")
|
||||
|
||||
#===================================================================
|
||||
# eoc
|
||||
#===================================================================
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
@@ -0,0 +1,363 @@
|
||||
"""passlib.handlers.sun_md5_crypt - Sun's Md5 Crypt, used on Solaris
|
||||
|
||||
.. warning::
|
||||
|
||||
This implementation may not reproduce
|
||||
the original Solaris behavior in some border cases.
|
||||
See documentation for details.
|
||||
"""
|
||||
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
# core
|
||||
from hashlib import md5
|
||||
import re
|
||||
import logging; log = logging.getLogger(__name__)
|
||||
from warnings import warn
|
||||
# site
|
||||
# pkg
|
||||
from passlib.utils import to_unicode
|
||||
from passlib.utils.binary import h64
|
||||
from passlib.utils.compat import byte_elem_value, irange, u, \
|
||||
uascii_to_str, unicode, str_to_bascii
|
||||
import passlib.utils.handlers as uh
|
||||
# local
|
||||
__all__ = [
|
||||
"sun_md5_crypt",
|
||||
]
|
||||
|
||||
#=============================================================================
|
||||
# backend
|
||||
#=============================================================================
|
||||
# constant data used by alg - Hamlet act 3 scene 1 + null char
|
||||
# exact bytes as in http://www.ibiblio.org/pub/docs/books/gutenberg/etext98/2ws2610.txt
|
||||
# from Project Gutenberg.
|
||||
|
||||
MAGIC_HAMLET = (
|
||||
b"To be, or not to be,--that is the question:--\n"
|
||||
b"Whether 'tis nobler in the mind to suffer\n"
|
||||
b"The slings and arrows of outrageous fortune\n"
|
||||
b"Or to take arms against a sea of troubles,\n"
|
||||
b"And by opposing end them?--To die,--to sleep,--\n"
|
||||
b"No more; and by a sleep to say we end\n"
|
||||
b"The heartache, and the thousand natural shocks\n"
|
||||
b"That flesh is heir to,--'tis a consummation\n"
|
||||
b"Devoutly to be wish'd. To die,--to sleep;--\n"
|
||||
b"To sleep! perchance to dream:--ay, there's the rub;\n"
|
||||
b"For in that sleep of death what dreams may come,\n"
|
||||
b"When we have shuffled off this mortal coil,\n"
|
||||
b"Must give us pause: there's the respect\n"
|
||||
b"That makes calamity of so long life;\n"
|
||||
b"For who would bear the whips and scorns of time,\n"
|
||||
b"The oppressor's wrong, the proud man's contumely,\n"
|
||||
b"The pangs of despis'd love, the law's delay,\n"
|
||||
b"The insolence of office, and the spurns\n"
|
||||
b"That patient merit of the unworthy takes,\n"
|
||||
b"When he himself might his quietus make\n"
|
||||
b"With a bare bodkin? who would these fardels bear,\n"
|
||||
b"To grunt and sweat under a weary life,\n"
|
||||
b"But that the dread of something after death,--\n"
|
||||
b"The undiscover'd country, from whose bourn\n"
|
||||
b"No traveller returns,--puzzles the will,\n"
|
||||
b"And makes us rather bear those ills we have\n"
|
||||
b"Than fly to others that we know not of?\n"
|
||||
b"Thus conscience does make cowards of us all;\n"
|
||||
b"And thus the native hue of resolution\n"
|
||||
b"Is sicklied o'er with the pale cast of thought;\n"
|
||||
b"And enterprises of great pith and moment,\n"
|
||||
b"With this regard, their currents turn awry,\n"
|
||||
b"And lose the name of action.--Soft you now!\n"
|
||||
b"The fair Ophelia!--Nymph, in thy orisons\n"
|
||||
b"Be all my sins remember'd.\n\x00" #<- apparently null at end of C string is included (test vector won't pass otherwise)
|
||||
)
|
||||
|
||||
# NOTE: these sequences are pre-calculated iteration ranges used by X & Y loops w/in rounds function below
|
||||
xr = irange(7)
|
||||
_XY_ROUNDS = [
|
||||
tuple((i,i,i+3) for i in xr), # xrounds 0
|
||||
tuple((i,i+1,i+4) for i in xr), # xrounds 1
|
||||
tuple((i,i+8,(i+11)&15) for i in xr), # yrounds 0
|
||||
tuple((i,(i+9)&15, (i+12)&15) for i in xr), # yrounds 1
|
||||
]
|
||||
del xr
|
||||
|
||||
def raw_sun_md5_crypt(secret, rounds, salt):
|
||||
"""given secret & salt, return encoded sun-md5-crypt checksum"""
|
||||
global MAGIC_HAMLET
|
||||
assert isinstance(secret, bytes)
|
||||
assert isinstance(salt, bytes)
|
||||
|
||||
# validate rounds
|
||||
if rounds <= 0:
|
||||
rounds = 0
|
||||
real_rounds = 4096 + rounds
|
||||
# NOTE: spec seems to imply max 'rounds' is 2**32-1
|
||||
|
||||
# generate initial digest to start off round 0.
|
||||
# NOTE: algorithm 'salt' includes full config string w/ trailing "$"
|
||||
result = md5(secret + salt).digest()
|
||||
assert len(result) == 16
|
||||
|
||||
# NOTE: many things in this function have been inlined (to speed up the loop
|
||||
# as much as possible), to the point that this code barely resembles
|
||||
# the algorithm as described in the docs. in particular:
|
||||
#
|
||||
# * all accesses to a given bit have been inlined using the formula
|
||||
# rbitval(bit) = (rval((bit>>3) & 15) >> (bit & 7)) & 1
|
||||
#
|
||||
# * the calculation of coinflip value R has been inlined
|
||||
#
|
||||
# * the conditional division of coinflip value V has been inlined as
|
||||
# a shift right of 0 or 1.
|
||||
#
|
||||
# * the i, i+3, etc iterations are precalculated in lists.
|
||||
#
|
||||
# * the round-based conditional division of x & y is now performed
|
||||
# by choosing an appropriate precalculated list, so that it only
|
||||
# calculates the 7 bits which will actually be used.
|
||||
#
|
||||
X_ROUNDS_0, X_ROUNDS_1, Y_ROUNDS_0, Y_ROUNDS_1 = _XY_ROUNDS
|
||||
|
||||
# NOTE: % appears to be *slightly* slower than &, so we prefer & if possible
|
||||
|
||||
round = 0
|
||||
while round < real_rounds:
|
||||
# convert last result byte string to list of byte-ints for easy access
|
||||
rval = [ byte_elem_value(c) for c in result ].__getitem__
|
||||
|
||||
# build up X bit by bit
|
||||
x = 0
|
||||
xrounds = X_ROUNDS_1 if (rval((round>>3) & 15)>>(round & 7)) & 1 else X_ROUNDS_0
|
||||
for i, ia, ib in xrounds:
|
||||
a = rval(ia)
|
||||
b = rval(ib)
|
||||
v = rval((a >> (b % 5)) & 15) >> ((b>>(a&7)) & 1)
|
||||
x |= ((rval((v>>3)&15)>>(v&7))&1) << i
|
||||
|
||||
# build up Y bit by bit
|
||||
y = 0
|
||||
yrounds = Y_ROUNDS_1 if (rval(((round+64)>>3) & 15)>>(round & 7)) & 1 else Y_ROUNDS_0
|
||||
for i, ia, ib in yrounds:
|
||||
a = rval(ia)
|
||||
b = rval(ib)
|
||||
v = rval((a >> (b % 5)) & 15) >> ((b>>(a&7)) & 1)
|
||||
y |= ((rval((v>>3)&15)>>(v&7))&1) << i
|
||||
|
||||
# extract x'th and y'th bit, xoring them together to yeild "coin flip"
|
||||
coin = ((rval(x>>3) >> (x&7)) ^ (rval(y>>3) >> (y&7))) & 1
|
||||
|
||||
# construct hash for this round
|
||||
h = md5(result)
|
||||
if coin:
|
||||
h.update(MAGIC_HAMLET)
|
||||
h.update(unicode(round).encode("ascii"))
|
||||
result = h.digest()
|
||||
|
||||
round += 1
|
||||
|
||||
# encode output
|
||||
return h64.encode_transposed_bytes(result, _chk_offsets)
|
||||
|
||||
# NOTE: same offsets as md5_crypt
|
||||
_chk_offsets = (
|
||||
12,6,0,
|
||||
13,7,1,
|
||||
14,8,2,
|
||||
15,9,3,
|
||||
5,10,4,
|
||||
11,
|
||||
)
|
||||
|
||||
#=============================================================================
|
||||
# handler
|
||||
#=============================================================================
|
||||
class sun_md5_crypt(uh.HasRounds, uh.HasSalt, uh.GenericHandler):
|
||||
"""This class implements the Sun-MD5-Crypt password hash, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports a variable-length salt, and a variable number of rounds.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
|
||||
|
||||
:type salt: str
|
||||
:param salt:
|
||||
Optional salt string.
|
||||
If not specified, a salt will be autogenerated (this is recommended).
|
||||
If specified, it must be drawn from the regexp range ``[./0-9A-Za-z]``.
|
||||
|
||||
:type salt_size: int
|
||||
:param salt_size:
|
||||
If no salt is specified, this parameter can be used to specify
|
||||
the size (in characters) of the autogenerated salt.
|
||||
It currently defaults to 8.
|
||||
|
||||
:type rounds: int
|
||||
:param rounds:
|
||||
Optional number of rounds to use.
|
||||
Defaults to 34000, must be between 0 and 4294963199, inclusive.
|
||||
|
||||
:type bare_salt: bool
|
||||
:param bare_salt:
|
||||
Optional flag used to enable an alternate salt digest behavior
|
||||
used by some hash strings in this scheme.
|
||||
This flag can be ignored by most users.
|
||||
Defaults to ``False``.
|
||||
(see :ref:`smc-bare-salt` for details).
|
||||
|
||||
:type relaxed: bool
|
||||
:param relaxed:
|
||||
By default, providing an invalid value for one of the other
|
||||
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
|
||||
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
|
||||
will be issued instead. Correctable errors include ``rounds``
|
||||
that are too small or too large, and ``salt`` strings that are too long.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
"""
|
||||
#===================================================================
|
||||
# class attrs
|
||||
#===================================================================
|
||||
name = "sun_md5_crypt"
|
||||
setting_kwds = ("salt", "rounds", "bare_salt", "salt_size")
|
||||
checksum_chars = uh.HASH64_CHARS
|
||||
checksum_size = 22
|
||||
|
||||
# NOTE: docs say max password length is 255.
|
||||
# release 9u2
|
||||
|
||||
# NOTE: not sure if original crypt has a salt size limit,
|
||||
# all instances that have been seen use 8 chars.
|
||||
default_salt_size = 8
|
||||
max_salt_size = None
|
||||
salt_chars = uh.HASH64_CHARS
|
||||
|
||||
default_rounds = 34000 # current passlib default
|
||||
min_rounds = 0
|
||||
max_rounds = 4294963199 ##2**32-1-4096
|
||||
# XXX: ^ not sure what it does if past this bound... does 32 int roll over?
|
||||
rounds_cost = "linear"
|
||||
|
||||
ident_values = (u("$md5$"), u("$md5,"))
|
||||
|
||||
#===================================================================
|
||||
# instance attrs
|
||||
#===================================================================
|
||||
bare_salt = False # flag to indicate legacy hashes that lack "$$" suffix
|
||||
|
||||
#===================================================================
|
||||
# constructor
|
||||
#===================================================================
|
||||
def __init__(self, bare_salt=False, **kwds):
|
||||
self.bare_salt = bare_salt
|
||||
super(sun_md5_crypt, self).__init__(**kwds)
|
||||
|
||||
#===================================================================
|
||||
# internal helpers
|
||||
#===================================================================
|
||||
@classmethod
|
||||
def identify(cls, hash):
|
||||
hash = uh.to_unicode_for_identify(hash)
|
||||
return hash.startswith(cls.ident_values)
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, hash):
|
||||
hash = to_unicode(hash, "ascii", "hash")
|
||||
|
||||
#
|
||||
# detect if hash specifies rounds value.
|
||||
# if so, parse and validate it.
|
||||
# by end, set 'rounds' to int value, and 'tail' containing salt+chk
|
||||
#
|
||||
if hash.startswith(u("$md5$")):
|
||||
rounds = 0
|
||||
salt_idx = 5
|
||||
elif hash.startswith(u("$md5,rounds=")):
|
||||
idx = hash.find(u("$"), 12)
|
||||
if idx == -1:
|
||||
raise uh.exc.MalformedHashError(cls, "unexpected end of rounds")
|
||||
rstr = hash[12:idx]
|
||||
try:
|
||||
rounds = int(rstr)
|
||||
except ValueError:
|
||||
raise uh.exc.MalformedHashError(cls, "bad rounds")
|
||||
if rstr != unicode(rounds):
|
||||
raise uh.exc.ZeroPaddedRoundsError(cls)
|
||||
if rounds == 0:
|
||||
# NOTE: not sure if this is forbidden by spec or not;
|
||||
# but allowing it would complicate things,
|
||||
# and it should never occur anyways.
|
||||
raise uh.exc.MalformedHashError(cls, "explicit zero rounds")
|
||||
salt_idx = idx+1
|
||||
else:
|
||||
raise uh.exc.InvalidHashError(cls)
|
||||
|
||||
#
|
||||
# salt/checksum separation is kinda weird,
|
||||
# to deal cleanly with some backward-compatible workarounds
|
||||
# implemented by original implementation.
|
||||
#
|
||||
chk_idx = hash.rfind(u("$"), salt_idx)
|
||||
if chk_idx == -1:
|
||||
# ''-config for $-hash
|
||||
salt = hash[salt_idx:]
|
||||
chk = None
|
||||
bare_salt = True
|
||||
elif chk_idx == len(hash)-1:
|
||||
if chk_idx > salt_idx and hash[-2] == u("$"):
|
||||
raise uh.exc.MalformedHashError(cls, "too many '$' separators")
|
||||
# $-config for $$-hash
|
||||
salt = hash[salt_idx:-1]
|
||||
chk = None
|
||||
bare_salt = False
|
||||
elif chk_idx > 0 and hash[chk_idx-1] == u("$"):
|
||||
# $$-hash
|
||||
salt = hash[salt_idx:chk_idx-1]
|
||||
chk = hash[chk_idx+1:]
|
||||
bare_salt = False
|
||||
else:
|
||||
# $-hash
|
||||
salt = hash[salt_idx:chk_idx]
|
||||
chk = hash[chk_idx+1:]
|
||||
bare_salt = True
|
||||
|
||||
return cls(
|
||||
rounds=rounds,
|
||||
salt=salt,
|
||||
checksum=chk,
|
||||
bare_salt=bare_salt,
|
||||
)
|
||||
|
||||
def to_string(self, _withchk=True):
|
||||
ss = u('') if self.bare_salt else u('$')
|
||||
rounds = self.rounds
|
||||
if rounds > 0:
|
||||
hash = u("$md5,rounds=%d$%s%s") % (rounds, self.salt, ss)
|
||||
else:
|
||||
hash = u("$md5$%s%s") % (self.salt, ss)
|
||||
if _withchk:
|
||||
chk = self.checksum
|
||||
hash = u("%s$%s") % (hash, chk)
|
||||
return uascii_to_str(hash)
|
||||
|
||||
#===================================================================
|
||||
# primary interface
|
||||
#===================================================================
|
||||
# TODO: if we're on solaris, check for native crypt() support.
|
||||
# this will require extra testing, to make sure native crypt
|
||||
# actually behaves correctly. of particular importance:
|
||||
# when using ""-config, make sure to append "$x" to string.
|
||||
|
||||
def _calc_checksum(self, secret):
|
||||
# NOTE: no reference for how sun_md5_crypt handles unicode
|
||||
if isinstance(secret, unicode):
|
||||
secret = secret.encode("utf-8")
|
||||
config = str_to_bascii(self.to_string(_withchk=False))
|
||||
return raw_sun_md5_crypt(secret, self.rounds, config).decode("ascii")
|
||||
|
||||
#===================================================================
|
||||
# eoc
|
||||
#===================================================================
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
334
venv/lib/python3.12/site-packages/passlib/handlers/windows.py
Normal file
334
venv/lib/python3.12/site-packages/passlib/handlers/windows.py
Normal file
@@ -0,0 +1,334 @@
|
||||
"""passlib.handlers.nthash - Microsoft Windows -related hashes"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
# core
|
||||
from binascii import hexlify
|
||||
import logging; log = logging.getLogger(__name__)
|
||||
from warnings import warn
|
||||
# site
|
||||
# pkg
|
||||
from passlib.utils import to_unicode, right_pad_string
|
||||
from passlib.utils.compat import unicode
|
||||
from passlib.crypto.digest import lookup_hash
|
||||
md4 = lookup_hash("md4").const
|
||||
import passlib.utils.handlers as uh
|
||||
# local
|
||||
__all__ = [
|
||||
"lmhash",
|
||||
"nthash",
|
||||
"bsd_nthash",
|
||||
"msdcc",
|
||||
"msdcc2",
|
||||
]
|
||||
|
||||
#=============================================================================
|
||||
# lanman hash
|
||||
#=============================================================================
|
||||
class lmhash(uh.TruncateMixin, uh.HasEncodingContext, uh.StaticHandler):
|
||||
"""This class implements the Lan Manager Password hash, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It has no salt and a single fixed round.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.using` method accepts a single
|
||||
optional keyword:
|
||||
|
||||
:param bool truncate_error:
|
||||
By default, this will silently truncate passwords larger than 14 bytes.
|
||||
Setting ``truncate_error=True`` will cause :meth:`~passlib.ifc.PasswordHash.hash`
|
||||
to raise a :exc:`~passlib.exc.PasswordTruncateError` instead.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.verify` methods accept a single
|
||||
optional keyword:
|
||||
|
||||
:type encoding: str
|
||||
:param encoding:
|
||||
|
||||
This specifies what character encoding LMHASH should use when
|
||||
calculating digest. It defaults to ``cp437``, the most
|
||||
common encoding encountered.
|
||||
|
||||
Note that while this class outputs digests in lower-case hexadecimal,
|
||||
it will accept upper-case as well.
|
||||
"""
|
||||
#===================================================================
|
||||
# class attrs
|
||||
#===================================================================
|
||||
|
||||
#--------------------
|
||||
# PasswordHash
|
||||
#--------------------
|
||||
name = "lmhash"
|
||||
setting_kwds = ("truncate_error",)
|
||||
|
||||
#--------------------
|
||||
# GenericHandler
|
||||
#--------------------
|
||||
checksum_chars = uh.HEX_CHARS
|
||||
checksum_size = 32
|
||||
|
||||
#--------------------
|
||||
# TruncateMixin
|
||||
#--------------------
|
||||
truncate_size = 14
|
||||
|
||||
#--------------------
|
||||
# custom
|
||||
#--------------------
|
||||
default_encoding = "cp437"
|
||||
|
||||
#===================================================================
|
||||
# methods
|
||||
#===================================================================
|
||||
@classmethod
|
||||
def _norm_hash(cls, hash):
|
||||
return hash.lower()
|
||||
|
||||
def _calc_checksum(self, secret):
|
||||
# check for truncation (during .hash() calls only)
|
||||
if self.use_defaults:
|
||||
self._check_truncate_policy(secret)
|
||||
|
||||
return hexlify(self.raw(secret, self.encoding)).decode("ascii")
|
||||
|
||||
# magic constant used by LMHASH
|
||||
_magic = b"KGS!@#$%"
|
||||
|
||||
@classmethod
|
||||
def raw(cls, secret, encoding=None):
|
||||
"""encode password using LANMAN hash algorithm.
|
||||
|
||||
:type secret: unicode or utf-8 encoded bytes
|
||||
:arg secret: secret to hash
|
||||
:type encoding: str
|
||||
:arg encoding:
|
||||
optional encoding to use for unicode inputs.
|
||||
this defaults to ``cp437``, which is the
|
||||
common case for most situations.
|
||||
|
||||
:returns: returns string of raw bytes
|
||||
"""
|
||||
if not encoding:
|
||||
encoding = cls.default_encoding
|
||||
# some nice empircal data re: different encodings is at...
|
||||
# http://www.openwall.com/lists/john-dev/2011/08/01/2
|
||||
# http://www.freerainbowtables.com/phpBB3/viewtopic.php?t=387&p=12163
|
||||
from passlib.crypto.des import des_encrypt_block
|
||||
MAGIC = cls._magic
|
||||
if isinstance(secret, unicode):
|
||||
# perform uppercasing while we're still unicode,
|
||||
# to give a better shot at getting non-ascii chars right.
|
||||
# (though some codepages do NOT upper-case the same as unicode).
|
||||
secret = secret.upper().encode(encoding)
|
||||
elif isinstance(secret, bytes):
|
||||
# FIXME: just trusting ascii upper will work?
|
||||
# and if not, how to do codepage specific case conversion?
|
||||
# we could decode first using <encoding>,
|
||||
# but *that* might not always be right.
|
||||
secret = secret.upper()
|
||||
else:
|
||||
raise TypeError("secret must be unicode or bytes")
|
||||
secret = right_pad_string(secret, 14)
|
||||
return des_encrypt_block(secret[0:7], MAGIC) + \
|
||||
des_encrypt_block(secret[7:14], MAGIC)
|
||||
|
||||
#===================================================================
|
||||
# eoc
|
||||
#===================================================================
|
||||
|
||||
#=============================================================================
|
||||
# ntlm hash
|
||||
#=============================================================================
|
||||
class nthash(uh.StaticHandler):
|
||||
"""This class implements the NT Password hash, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It has no salt and a single fixed round.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept no optional keywords.
|
||||
|
||||
Note that while this class outputs lower-case hexadecimal digests,
|
||||
it will accept upper-case digests as well.
|
||||
"""
|
||||
#===================================================================
|
||||
# class attrs
|
||||
#===================================================================
|
||||
name = "nthash"
|
||||
checksum_chars = uh.HEX_CHARS
|
||||
checksum_size = 32
|
||||
|
||||
#===================================================================
|
||||
# methods
|
||||
#===================================================================
|
||||
@classmethod
|
||||
def _norm_hash(cls, hash):
|
||||
return hash.lower()
|
||||
|
||||
def _calc_checksum(self, secret):
|
||||
return hexlify(self.raw(secret)).decode("ascii")
|
||||
|
||||
@classmethod
|
||||
def raw(cls, secret):
|
||||
"""encode password using MD4-based NTHASH algorithm
|
||||
|
||||
:arg secret: secret as unicode or utf-8 encoded bytes
|
||||
|
||||
:returns: returns string of raw bytes
|
||||
"""
|
||||
secret = to_unicode(secret, "utf-8", param="secret")
|
||||
# XXX: found refs that say only first 128 chars are used.
|
||||
return md4(secret.encode("utf-16-le")).digest()
|
||||
|
||||
@classmethod
|
||||
def raw_nthash(cls, secret, hex=False):
|
||||
warn("nthash.raw_nthash() is deprecated, and will be removed "
|
||||
"in Passlib 1.8, please use nthash.raw() instead",
|
||||
DeprecationWarning)
|
||||
ret = nthash.raw(secret)
|
||||
return hexlify(ret).decode("ascii") if hex else ret
|
||||
|
||||
#===================================================================
|
||||
# eoc
|
||||
#===================================================================
|
||||
|
||||
bsd_nthash = uh.PrefixWrapper("bsd_nthash", nthash, prefix="$3$$", ident="$3$$",
|
||||
doc="""The class support FreeBSD's representation of NTHASH
|
||||
(which is compatible with the :ref:`modular-crypt-format`),
|
||||
and follows the :ref:`password-hash-api`.
|
||||
|
||||
It has no salt and a single fixed round.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept no optional keywords.
|
||||
""")
|
||||
|
||||
##class ntlm_pair(object):
|
||||
## "combined lmhash & nthash"
|
||||
## name = "ntlm_pair"
|
||||
## setting_kwds = ()
|
||||
## _hash_regex = re.compile(u"^(?P<lm>[0-9a-f]{32}):(?P<nt>[0-9][a-f]{32})$",
|
||||
## re.I)
|
||||
##
|
||||
## @classmethod
|
||||
## def identify(cls, hash):
|
||||
## hash = to_unicode(hash, "latin-1", "hash")
|
||||
## return len(hash) == 65 and cls._hash_regex.match(hash) is not None
|
||||
##
|
||||
## @classmethod
|
||||
## def hash(cls, secret, config=None):
|
||||
## if config is not None and not cls.identify(config):
|
||||
## raise uh.exc.InvalidHashError(cls)
|
||||
## return lmhash.hash(secret) + ":" + nthash.hash(secret)
|
||||
##
|
||||
## @classmethod
|
||||
## def verify(cls, secret, hash):
|
||||
## hash = to_unicode(hash, "ascii", "hash")
|
||||
## m = cls._hash_regex.match(hash)
|
||||
## if not m:
|
||||
## raise uh.exc.InvalidHashError(cls)
|
||||
## lm, nt = m.group("lm", "nt")
|
||||
## # NOTE: verify against both in case encoding issue
|
||||
## # causes one not to match.
|
||||
## return lmhash.verify(secret, lm) or nthash.verify(secret, nt)
|
||||
|
||||
#=============================================================================
|
||||
# msdcc v1
|
||||
#=============================================================================
|
||||
class msdcc(uh.HasUserContext, uh.StaticHandler):
|
||||
"""This class implements Microsoft's Domain Cached Credentials password hash,
|
||||
and follows the :ref:`password-hash-api`.
|
||||
|
||||
It has a fixed number of rounds, and uses the associated
|
||||
username as the salt.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.hash`, :meth:`~passlib.ifc.PasswordHash.genhash`, and :meth:`~passlib.ifc.PasswordHash.verify` methods
|
||||
have the following optional keywords:
|
||||
|
||||
:type user: str
|
||||
:param user:
|
||||
String containing name of user account this password is associated with.
|
||||
This is required to properly calculate the hash.
|
||||
|
||||
This keyword is case-insensitive, and should contain just the username
|
||||
(e.g. ``Administrator``, not ``SOMEDOMAIN\\Administrator``).
|
||||
|
||||
Note that while this class outputs lower-case hexadecimal digests,
|
||||
it will accept upper-case digests as well.
|
||||
"""
|
||||
name = "msdcc"
|
||||
checksum_chars = uh.HEX_CHARS
|
||||
checksum_size = 32
|
||||
|
||||
@classmethod
|
||||
def _norm_hash(cls, hash):
|
||||
return hash.lower()
|
||||
|
||||
def _calc_checksum(self, secret):
|
||||
return hexlify(self.raw(secret, self.user)).decode("ascii")
|
||||
|
||||
@classmethod
|
||||
def raw(cls, secret, user):
|
||||
"""encode password using mscash v1 algorithm
|
||||
|
||||
:arg secret: secret as unicode or utf-8 encoded bytes
|
||||
:arg user: username to use as salt
|
||||
|
||||
:returns: returns string of raw bytes
|
||||
"""
|
||||
secret = to_unicode(secret, "utf-8", param="secret").encode("utf-16-le")
|
||||
user = to_unicode(user, "utf-8", param="user").lower().encode("utf-16-le")
|
||||
return md4(md4(secret).digest() + user).digest()
|
||||
|
||||
#=============================================================================
|
||||
# msdcc2 aka mscash2
|
||||
#=============================================================================
|
||||
class msdcc2(uh.HasUserContext, uh.StaticHandler):
|
||||
"""This class implements version 2 of Microsoft's Domain Cached Credentials
|
||||
password hash, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It has a fixed number of rounds, and uses the associated
|
||||
username as the salt.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.hash`, :meth:`~passlib.ifc.PasswordHash.genhash`, and :meth:`~passlib.ifc.PasswordHash.verify` methods
|
||||
have the following extra keyword:
|
||||
|
||||
:type user: str
|
||||
:param user:
|
||||
String containing name of user account this password is associated with.
|
||||
This is required to properly calculate the hash.
|
||||
|
||||
This keyword is case-insensitive, and should contain just the username
|
||||
(e.g. ``Administrator``, not ``SOMEDOMAIN\\Administrator``).
|
||||
"""
|
||||
name = "msdcc2"
|
||||
checksum_chars = uh.HEX_CHARS
|
||||
checksum_size = 32
|
||||
|
||||
@classmethod
|
||||
def _norm_hash(cls, hash):
|
||||
return hash.lower()
|
||||
|
||||
def _calc_checksum(self, secret):
|
||||
return hexlify(self.raw(secret, self.user)).decode("ascii")
|
||||
|
||||
@classmethod
|
||||
def raw(cls, secret, user):
|
||||
"""encode password using msdcc v2 algorithm
|
||||
|
||||
:type secret: unicode or utf-8 bytes
|
||||
:arg secret: secret
|
||||
|
||||
:type user: str
|
||||
:arg user: username to use as salt
|
||||
|
||||
:returns: returns string of raw bytes
|
||||
"""
|
||||
from passlib.crypto.digest import pbkdf2_hmac
|
||||
secret = to_unicode(secret, "utf-8", param="secret").encode("utf-16-le")
|
||||
user = to_unicode(user, "utf-8", param="user").lower().encode("utf-16-le")
|
||||
tmp = md4(md4(secret).digest() + user).digest()
|
||||
return pbkdf2_hmac("sha1", tmp, user, 10240, 16)
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
Reference in New Issue
Block a user