Загрузить файлы в «venv/Lib/site-packages/passlib/handlers»

This commit is contained in:
2026-07-02 18:49:21 +00:00
parent 352f46e58c
commit 78595c650d
5 changed files with 1288 additions and 0 deletions

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