1
0
mirror of https://gitlab.com/MoonTestUse1/AdministrationItDepartmens.git synced 2025-08-14 00:25:46 +02:00

Initial commit

This commit is contained in:
MoonTestUse1
2024-12-23 19:27:44 +06:00
commit e81df4c87e
4952 changed files with 1705479 additions and 0 deletions

View File

@@ -0,0 +1 @@
"""passlib.handlers -- holds implementations of all passlib's builtin hash formats"""

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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
#=============================================================================

View 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
#=============================================================================

View 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
#=============================================================================

View 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
#=============================================================================

View 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
#=============================================================================

View File

@@ -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
#=============================================================================

View 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
#=============================================================================

View 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
#=============================================================================

View 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
#=============================================================================

View 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
#=============================================================================

View 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
#=============================================================================

View 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
#=============================================================================

View 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
#=============================================================================

View File

@@ -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
#=============================================================================

View File

@@ -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
#=============================================================================

View 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
#=============================================================================

View 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
#=============================================================================

View 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
#=============================================================================

View 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
#=============================================================================

View File

@@ -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
#=============================================================================

View 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
#=============================================================================