Загрузить файлы в «venv/Lib/site-packages/passlib/handlers»
This commit is contained in:
135
venv/Lib/site-packages/passlib/handlers/phpass.py
Normal file
135
venv/Lib/site-packages/passlib/handlers/phpass.py
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
"""passlib.handlers.phpass - PHPass Portable Crypt
|
||||||
|
|
||||||
|
phppass located - http://www.openwall.com/phpass/
|
||||||
|
algorithm described - http://www.openwall.com/articles/PHP-Users-Passwords
|
||||||
|
|
||||||
|
phpass context - blowfish, bsdi_crypt, phpass
|
||||||
|
"""
|
||||||
|
#=============================================================================
|
||||||
|
# imports
|
||||||
|
#=============================================================================
|
||||||
|
# core
|
||||||
|
from hashlib import md5
|
||||||
|
import logging; log = logging.getLogger(__name__)
|
||||||
|
# site
|
||||||
|
# pkg
|
||||||
|
from passlib.utils.binary import h64
|
||||||
|
from passlib.utils.compat import u, uascii_to_str, unicode
|
||||||
|
import passlib.utils.handlers as uh
|
||||||
|
# local
|
||||||
|
__all__ = [
|
||||||
|
"phpass",
|
||||||
|
]
|
||||||
|
|
||||||
|
#=============================================================================
|
||||||
|
# phpass
|
||||||
|
#=============================================================================
|
||||||
|
class phpass(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.GenericHandler):
|
||||||
|
"""This class implements the PHPass Portable Hash, and follows the :ref:`password-hash-api`.
|
||||||
|
|
||||||
|
It supports a fixed-length salt, and a variable number of rounds.
|
||||||
|
|
||||||
|
The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
|
||||||
|
|
||||||
|
:type salt: str
|
||||||
|
:param salt:
|
||||||
|
Optional salt string.
|
||||||
|
If not specified, one will be autogenerated (this is recommended).
|
||||||
|
If specified, it must be 8 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
|
||||||
|
|
||||||
|
:type rounds: int
|
||||||
|
:param rounds:
|
||||||
|
Optional number of rounds to use.
|
||||||
|
Defaults to 19, must be between 7 and 30, inclusive.
|
||||||
|
This value is logarithmic, the actual number of iterations used will be :samp:`2**{rounds}`.
|
||||||
|
|
||||||
|
:type ident: str
|
||||||
|
:param ident:
|
||||||
|
phpBB3 uses ``H`` instead of ``P`` for its identifier,
|
||||||
|
this may be set to ``H`` in order to generate phpBB3 compatible hashes.
|
||||||
|
it defaults to ``P``.
|
||||||
|
|
||||||
|
:type relaxed: bool
|
||||||
|
:param relaxed:
|
||||||
|
By default, providing an invalid value for one of the other
|
||||||
|
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
|
||||||
|
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
|
||||||
|
will be issued instead. Correctable errors include ``rounds``
|
||||||
|
that are too small or too large, and ``salt`` strings that are too long.
|
||||||
|
|
||||||
|
.. versionadded:: 1.6
|
||||||
|
"""
|
||||||
|
|
||||||
|
#===================================================================
|
||||||
|
# class attrs
|
||||||
|
#===================================================================
|
||||||
|
#--GenericHandler--
|
||||||
|
name = "phpass"
|
||||||
|
setting_kwds = ("salt", "rounds", "ident")
|
||||||
|
checksum_chars = uh.HASH64_CHARS
|
||||||
|
|
||||||
|
#--HasSalt--
|
||||||
|
min_salt_size = max_salt_size = 8
|
||||||
|
salt_chars = uh.HASH64_CHARS
|
||||||
|
|
||||||
|
#--HasRounds--
|
||||||
|
default_rounds = 19
|
||||||
|
min_rounds = 7
|
||||||
|
max_rounds = 30
|
||||||
|
rounds_cost = "log2"
|
||||||
|
|
||||||
|
#--HasManyIdents--
|
||||||
|
default_ident = u("$P$")
|
||||||
|
ident_values = (u("$P$"), u("$H$"))
|
||||||
|
ident_aliases = {u("P"):u("$P$"), u("H"):u("$H$")}
|
||||||
|
|
||||||
|
#===================================================================
|
||||||
|
# formatting
|
||||||
|
#===================================================================
|
||||||
|
|
||||||
|
#$P$9IQRaTwmfeRo7ud9Fh4E2PdI0S3r.L0
|
||||||
|
# $P$
|
||||||
|
# 9
|
||||||
|
# IQRaTwmf
|
||||||
|
# eRo7ud9Fh4E2PdI0S3r.L0
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_string(cls, hash):
|
||||||
|
ident, data = cls._parse_ident(hash)
|
||||||
|
rounds, salt, chk = data[0], data[1:9], data[9:]
|
||||||
|
return cls(
|
||||||
|
ident=ident,
|
||||||
|
rounds=h64.decode_int6(rounds.encode("ascii")),
|
||||||
|
salt=salt,
|
||||||
|
checksum=chk or None,
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_string(self):
|
||||||
|
hash = u("%s%s%s%s") % (self.ident,
|
||||||
|
h64.encode_int6(self.rounds).decode("ascii"),
|
||||||
|
self.salt,
|
||||||
|
self.checksum or u(''))
|
||||||
|
return uascii_to_str(hash)
|
||||||
|
|
||||||
|
#===================================================================
|
||||||
|
# backend
|
||||||
|
#===================================================================
|
||||||
|
def _calc_checksum(self, secret):
|
||||||
|
# FIXME: can't find definitive policy on how phpass handles non-ascii.
|
||||||
|
if isinstance(secret, unicode):
|
||||||
|
secret = secret.encode("utf-8")
|
||||||
|
real_rounds = 1<<self.rounds
|
||||||
|
result = md5(self.salt.encode("ascii") + secret).digest()
|
||||||
|
r = 0
|
||||||
|
while r < real_rounds:
|
||||||
|
result = md5(result + secret).digest()
|
||||||
|
r += 1
|
||||||
|
return h64.encode_bytes(result).decode("ascii")
|
||||||
|
|
||||||
|
#===================================================================
|
||||||
|
# eoc
|
||||||
|
#===================================================================
|
||||||
|
|
||||||
|
#=============================================================================
|
||||||
|
# eof
|
||||||
|
#=============================================================================
|
||||||
55
venv/Lib/site-packages/passlib/handlers/postgres.py
Normal file
55
venv/Lib/site-packages/passlib/handlers/postgres.py
Normal 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
|
||||||
|
#=============================================================================
|
||||||
29
venv/Lib/site-packages/passlib/handlers/roundup.py
Normal file
29
venv/Lib/site-packages/passlib/handlers/roundup.py
Normal 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
|
||||||
|
#=============================================================================
|
||||||
582
venv/Lib/site-packages/passlib/handlers/scram.py
Normal file
582
venv/Lib/site-packages/passlib/handlers/scram.py
Normal file
@@ -0,0 +1,582 @@
|
|||||||
|
"""passlib.handlers.scram - hash for SCRAM credential storage"""
|
||||||
|
#=============================================================================
|
||||||
|
# imports
|
||||||
|
#=============================================================================
|
||||||
|
# core
|
||||||
|
import logging; log = logging.getLogger(__name__)
|
||||||
|
# site
|
||||||
|
# pkg
|
||||||
|
from passlib.utils import consteq, saslprep, to_native_str, splitcomma
|
||||||
|
from passlib.utils.binary import ab64_decode, ab64_encode
|
||||||
|
from passlib.utils.compat import bascii_to_str, iteritems, u, native_string_types
|
||||||
|
from passlib.crypto.digest import pbkdf2_hmac, norm_hash_name
|
||||||
|
import passlib.utils.handlers as uh
|
||||||
|
# local
|
||||||
|
__all__ = [
|
||||||
|
"scram",
|
||||||
|
]
|
||||||
|
|
||||||
|
#=============================================================================
|
||||||
|
# scram credentials hash
|
||||||
|
#=============================================================================
|
||||||
|
class scram(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
|
||||||
|
"""This class provides a format for storing SCRAM passwords, and follows
|
||||||
|
the :ref:`password-hash-api`.
|
||||||
|
|
||||||
|
It supports a variable-length salt, and a variable number of rounds.
|
||||||
|
|
||||||
|
The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
|
||||||
|
|
||||||
|
:type salt: bytes
|
||||||
|
:param salt:
|
||||||
|
Optional salt bytes.
|
||||||
|
If specified, the length must be between 0-1024 bytes.
|
||||||
|
If not specified, a 12 byte salt will be autogenerated
|
||||||
|
(this is recommended).
|
||||||
|
|
||||||
|
:type salt_size: int
|
||||||
|
:param salt_size:
|
||||||
|
Optional number of bytes to use when autogenerating new salts.
|
||||||
|
Defaults to 12 bytes, but can be any value between 0 and 1024.
|
||||||
|
|
||||||
|
:type rounds: int
|
||||||
|
:param rounds:
|
||||||
|
Optional number of rounds to use.
|
||||||
|
Defaults to 100000, but must be within ``range(1,1<<32)``.
|
||||||
|
|
||||||
|
:type algs: list of strings
|
||||||
|
:param algs:
|
||||||
|
Specify list of digest algorithms to use.
|
||||||
|
|
||||||
|
By default each scram hash will contain digests for SHA-1,
|
||||||
|
SHA-256, and SHA-512. This can be overridden by specify either be a
|
||||||
|
list such as ``["sha-1", "sha-256"]``, or a comma-separated string
|
||||||
|
such as ``"sha-1, sha-256"``. Names are case insensitive, and may
|
||||||
|
use :mod:`!hashlib` or `IANA <http://www.iana.org/assignments/hash-function-text-names>`_
|
||||||
|
hash names.
|
||||||
|
|
||||||
|
:type relaxed: bool
|
||||||
|
:param relaxed:
|
||||||
|
By default, providing an invalid value for one of the other
|
||||||
|
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
|
||||||
|
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
|
||||||
|
will be issued instead. Correctable errors include ``rounds``
|
||||||
|
that are too small or too large, and ``salt`` strings that are too long.
|
||||||
|
|
||||||
|
.. versionadded:: 1.6
|
||||||
|
|
||||||
|
In addition to the standard :ref:`password-hash-api` methods,
|
||||||
|
this class also provides the following methods for manipulating Passlib
|
||||||
|
scram hashes in ways useful for pluging into a SCRAM protocol stack:
|
||||||
|
|
||||||
|
.. automethod:: extract_digest_info
|
||||||
|
.. automethod:: extract_digest_algs
|
||||||
|
.. automethod:: derive_digest
|
||||||
|
"""
|
||||||
|
#===================================================================
|
||||||
|
# class attrs
|
||||||
|
#===================================================================
|
||||||
|
|
||||||
|
# NOTE: unlike most GenericHandler classes, the 'checksum' attr of
|
||||||
|
# ScramHandler is actually a map from digest_name -> digest, so
|
||||||
|
# many of the standard methods have been overridden.
|
||||||
|
|
||||||
|
# NOTE: max_salt_size and max_rounds are arbitrarily chosen to provide
|
||||||
|
# a sanity check; the underlying pbkdf2 specifies no bounds for either.
|
||||||
|
|
||||||
|
#--GenericHandler--
|
||||||
|
name = "scram"
|
||||||
|
setting_kwds = ("salt", "salt_size", "rounds", "algs")
|
||||||
|
ident = u("$scram$")
|
||||||
|
|
||||||
|
#--HasSalt--
|
||||||
|
default_salt_size = 12
|
||||||
|
max_salt_size = 1024
|
||||||
|
|
||||||
|
#--HasRounds--
|
||||||
|
default_rounds = 100000
|
||||||
|
min_rounds = 1
|
||||||
|
max_rounds = 2**32-1
|
||||||
|
rounds_cost = "linear"
|
||||||
|
|
||||||
|
#--custom--
|
||||||
|
|
||||||
|
# default algorithms when creating new hashes.
|
||||||
|
default_algs = ["sha-1", "sha-256", "sha-512"]
|
||||||
|
|
||||||
|
# list of algs verify prefers to use, in order.
|
||||||
|
_verify_algs = ["sha-256", "sha-512", "sha-224", "sha-384", "sha-1"]
|
||||||
|
|
||||||
|
#===================================================================
|
||||||
|
# instance attrs
|
||||||
|
#===================================================================
|
||||||
|
|
||||||
|
# 'checksum' is different from most GenericHandler subclasses,
|
||||||
|
# in that it contains a dict mapping from alg -> digest,
|
||||||
|
# or None if no checksum present.
|
||||||
|
|
||||||
|
# list of algorithms to create/compare digests for.
|
||||||
|
algs = None
|
||||||
|
|
||||||
|
#===================================================================
|
||||||
|
# scram frontend helpers
|
||||||
|
#===================================================================
|
||||||
|
@classmethod
|
||||||
|
def extract_digest_info(cls, hash, alg):
|
||||||
|
"""return (salt, rounds, digest) for specific hash algorithm.
|
||||||
|
|
||||||
|
:type hash: str
|
||||||
|
:arg hash:
|
||||||
|
:class:`!scram` hash stored for desired user
|
||||||
|
|
||||||
|
:type alg: str
|
||||||
|
:arg alg:
|
||||||
|
Name of digest algorithm (e.g. ``"sha-1"``) requested by client.
|
||||||
|
|
||||||
|
This value is run through :func:`~passlib.crypto.digest.norm_hash_name`,
|
||||||
|
so it is case-insensitive, and can be the raw SCRAM
|
||||||
|
mechanism name (e.g. ``"SCRAM-SHA-1"``), the IANA name,
|
||||||
|
or the hashlib name.
|
||||||
|
|
||||||
|
:raises KeyError:
|
||||||
|
If the hash does not contain an entry for the requested digest
|
||||||
|
algorithm.
|
||||||
|
|
||||||
|
:returns:
|
||||||
|
A tuple containing ``(salt, rounds, digest)``,
|
||||||
|
where *digest* matches the raw bytes returned by
|
||||||
|
SCRAM's :func:`Hi` function for the stored password,
|
||||||
|
the provided *salt*, and the iteration count (*rounds*).
|
||||||
|
*salt* and *digest* are both raw (unencoded) bytes.
|
||||||
|
"""
|
||||||
|
# XXX: this could be sped up by writing custom parsing routine
|
||||||
|
# that just picks out relevant digest, and doesn't bother
|
||||||
|
# with full structure validation each time it's called.
|
||||||
|
alg = norm_hash_name(alg, 'iana')
|
||||||
|
self = cls.from_string(hash)
|
||||||
|
chkmap = self.checksum
|
||||||
|
if not chkmap:
|
||||||
|
raise ValueError("scram hash contains no digests")
|
||||||
|
return self.salt, self.rounds, chkmap[alg]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def extract_digest_algs(cls, hash, format="iana"):
|
||||||
|
"""Return names of all algorithms stored in a given hash.
|
||||||
|
|
||||||
|
:type hash: str
|
||||||
|
:arg hash:
|
||||||
|
The :class:`!scram` hash to parse
|
||||||
|
|
||||||
|
:type format: str
|
||||||
|
:param format:
|
||||||
|
This changes the naming convention used by the
|
||||||
|
returned algorithm names. By default the names
|
||||||
|
are IANA-compatible; possible values are ``"iana"`` or ``"hashlib"``.
|
||||||
|
|
||||||
|
:returns:
|
||||||
|
Returns a list of digest algorithms; e.g. ``["sha-1"]``
|
||||||
|
"""
|
||||||
|
# XXX: this could be sped up by writing custom parsing routine
|
||||||
|
# that just picks out relevant names, and doesn't bother
|
||||||
|
# with full structure validation each time it's called.
|
||||||
|
algs = cls.from_string(hash).algs
|
||||||
|
if format == "iana":
|
||||||
|
return algs
|
||||||
|
else:
|
||||||
|
return [norm_hash_name(alg, format) for alg in algs]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def derive_digest(cls, password, salt, rounds, alg):
|
||||||
|
"""helper to create SaltedPassword digest for SCRAM.
|
||||||
|
|
||||||
|
This performs the step in the SCRAM protocol described as::
|
||||||
|
|
||||||
|
SaltedPassword := Hi(Normalize(password), salt, i)
|
||||||
|
|
||||||
|
:type password: unicode or utf-8 bytes
|
||||||
|
:arg password: password to run through digest
|
||||||
|
|
||||||
|
:type salt: bytes
|
||||||
|
:arg salt: raw salt data
|
||||||
|
|
||||||
|
:type rounds: int
|
||||||
|
:arg rounds: number of iterations.
|
||||||
|
|
||||||
|
:type alg: str
|
||||||
|
:arg alg: name of digest to use (e.g. ``"sha-1"``).
|
||||||
|
|
||||||
|
:returns:
|
||||||
|
raw bytes of ``SaltedPassword``
|
||||||
|
"""
|
||||||
|
if isinstance(password, bytes):
|
||||||
|
password = password.decode("utf-8")
|
||||||
|
# NOTE: pbkdf2_hmac() will encode secret & salt using utf-8,
|
||||||
|
# and handle normalizing alg name.
|
||||||
|
return pbkdf2_hmac(alg, saslprep(password), salt, rounds)
|
||||||
|
|
||||||
|
#===================================================================
|
||||||
|
# serialization
|
||||||
|
#===================================================================
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_string(cls, hash):
|
||||||
|
hash = to_native_str(hash, "ascii", "hash")
|
||||||
|
if not hash.startswith("$scram$"):
|
||||||
|
raise uh.exc.InvalidHashError(cls)
|
||||||
|
parts = hash[7:].split("$")
|
||||||
|
if len(parts) != 3:
|
||||||
|
raise uh.exc.MalformedHashError(cls)
|
||||||
|
rounds_str, salt_str, chk_str = parts
|
||||||
|
|
||||||
|
# decode rounds
|
||||||
|
rounds = int(rounds_str)
|
||||||
|
if rounds_str != str(rounds): # forbid zero padding, etc.
|
||||||
|
raise uh.exc.MalformedHashError(cls)
|
||||||
|
|
||||||
|
# decode salt
|
||||||
|
try:
|
||||||
|
salt = ab64_decode(salt_str.encode("ascii"))
|
||||||
|
except TypeError:
|
||||||
|
raise uh.exc.MalformedHashError(cls)
|
||||||
|
|
||||||
|
# decode algs/digest list
|
||||||
|
if not chk_str:
|
||||||
|
# scram hashes MUST have something here.
|
||||||
|
raise uh.exc.MalformedHashError(cls)
|
||||||
|
elif "=" in chk_str:
|
||||||
|
# comma-separated list of 'alg=digest' pairs
|
||||||
|
algs = None
|
||||||
|
chkmap = {}
|
||||||
|
for pair in chk_str.split(","):
|
||||||
|
alg, digest = pair.split("=")
|
||||||
|
try:
|
||||||
|
chkmap[alg] = ab64_decode(digest.encode("ascii"))
|
||||||
|
except TypeError:
|
||||||
|
raise uh.exc.MalformedHashError(cls)
|
||||||
|
else:
|
||||||
|
# comma-separated list of alg names, no digests
|
||||||
|
algs = chk_str
|
||||||
|
chkmap = None
|
||||||
|
|
||||||
|
# return new object
|
||||||
|
return cls(
|
||||||
|
rounds=rounds,
|
||||||
|
salt=salt,
|
||||||
|
checksum=chkmap,
|
||||||
|
algs=algs,
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_string(self):
|
||||||
|
salt = bascii_to_str(ab64_encode(self.salt))
|
||||||
|
chkmap = self.checksum
|
||||||
|
chk_str = ",".join(
|
||||||
|
"%s=%s" % (alg, bascii_to_str(ab64_encode(chkmap[alg])))
|
||||||
|
for alg in self.algs
|
||||||
|
)
|
||||||
|
return '$scram$%d$%s$%s' % (self.rounds, salt, chk_str)
|
||||||
|
|
||||||
|
#===================================================================
|
||||||
|
# variant constructor
|
||||||
|
#===================================================================
|
||||||
|
@classmethod
|
||||||
|
def using(cls, default_algs=None, algs=None, **kwds):
|
||||||
|
# parse aliases
|
||||||
|
if algs is not None:
|
||||||
|
assert default_algs is None
|
||||||
|
default_algs = algs
|
||||||
|
|
||||||
|
# create subclass
|
||||||
|
subcls = super(scram, cls).using(**kwds)
|
||||||
|
|
||||||
|
# fill in algs
|
||||||
|
if default_algs is not None:
|
||||||
|
subcls.default_algs = cls._norm_algs(default_algs)
|
||||||
|
return subcls
|
||||||
|
|
||||||
|
#===================================================================
|
||||||
|
# init
|
||||||
|
#===================================================================
|
||||||
|
def __init__(self, algs=None, **kwds):
|
||||||
|
super(scram, self).__init__(**kwds)
|
||||||
|
|
||||||
|
# init algs
|
||||||
|
digest_map = self.checksum
|
||||||
|
if algs is not None:
|
||||||
|
if digest_map is not None:
|
||||||
|
raise RuntimeError("checksum & algs kwds are mutually exclusive")
|
||||||
|
algs = self._norm_algs(algs)
|
||||||
|
elif digest_map is not None:
|
||||||
|
# derive algs list from digest map (if present).
|
||||||
|
algs = self._norm_algs(digest_map.keys())
|
||||||
|
elif self.use_defaults:
|
||||||
|
algs = list(self.default_algs)
|
||||||
|
assert self._norm_algs(algs) == algs, "invalid default algs: %r" % (algs,)
|
||||||
|
else:
|
||||||
|
raise TypeError("no algs list specified")
|
||||||
|
self.algs = algs
|
||||||
|
|
||||||
|
def _norm_checksum(self, checksum, relaxed=False):
|
||||||
|
if not isinstance(checksum, dict):
|
||||||
|
raise uh.exc.ExpectedTypeError(checksum, "dict", "checksum")
|
||||||
|
for alg, digest in iteritems(checksum):
|
||||||
|
if alg != norm_hash_name(alg, 'iana'):
|
||||||
|
raise ValueError("malformed algorithm name in scram hash: %r" %
|
||||||
|
(alg,))
|
||||||
|
if len(alg) > 9:
|
||||||
|
raise ValueError("SCRAM limits algorithm names to "
|
||||||
|
"9 characters: %r" % (alg,))
|
||||||
|
if not isinstance(digest, bytes):
|
||||||
|
raise uh.exc.ExpectedTypeError(digest, "raw bytes", "digests")
|
||||||
|
# TODO: verify digest size (if digest is known)
|
||||||
|
if 'sha-1' not in checksum:
|
||||||
|
# NOTE: required because of SCRAM spec.
|
||||||
|
raise ValueError("sha-1 must be in algorithm list of scram hash")
|
||||||
|
return checksum
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _norm_algs(cls, algs):
|
||||||
|
"""normalize algs parameter"""
|
||||||
|
if isinstance(algs, native_string_types):
|
||||||
|
algs = splitcomma(algs)
|
||||||
|
algs = sorted(norm_hash_name(alg, 'iana') for alg in algs)
|
||||||
|
if any(len(alg)>9 for alg in algs):
|
||||||
|
raise ValueError("SCRAM limits alg names to max of 9 characters")
|
||||||
|
if 'sha-1' not in algs:
|
||||||
|
# NOTE: required because of SCRAM spec (rfc 5802)
|
||||||
|
raise ValueError("sha-1 must be in algorithm list of scram hash")
|
||||||
|
return algs
|
||||||
|
|
||||||
|
#===================================================================
|
||||||
|
# migration
|
||||||
|
#===================================================================
|
||||||
|
def _calc_needs_update(self, **kwds):
|
||||||
|
# marks hashes as deprecated if they don't include at least all default_algs.
|
||||||
|
# XXX: should we deprecate if they aren't exactly the same,
|
||||||
|
# to permit removing legacy hashes?
|
||||||
|
if not set(self.algs).issuperset(self.default_algs):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# hand off to base implementation
|
||||||
|
return super(scram, self)._calc_needs_update(**kwds)
|
||||||
|
|
||||||
|
#===================================================================
|
||||||
|
# digest methods
|
||||||
|
#===================================================================
|
||||||
|
def _calc_checksum(self, secret, alg=None):
|
||||||
|
rounds = self.rounds
|
||||||
|
salt = self.salt
|
||||||
|
hash = self.derive_digest
|
||||||
|
if alg:
|
||||||
|
# if requested, generate digest for specific alg
|
||||||
|
return hash(secret, salt, rounds, alg)
|
||||||
|
else:
|
||||||
|
# by default, return dict containing digests for all algs
|
||||||
|
return dict(
|
||||||
|
(alg, hash(secret, salt, rounds, alg))
|
||||||
|
for alg in self.algs
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def verify(cls, secret, hash, full=False):
|
||||||
|
uh.validate_secret(secret)
|
||||||
|
self = cls.from_string(hash)
|
||||||
|
chkmap = self.checksum
|
||||||
|
if not chkmap:
|
||||||
|
raise ValueError("expected %s hash, got %s config string instead" %
|
||||||
|
(cls.name, cls.name))
|
||||||
|
|
||||||
|
# NOTE: to make the verify method efficient, we just calculate hash
|
||||||
|
# of shortest digest by default. apps can pass in "full=True" to
|
||||||
|
# check entire hash for consistency.
|
||||||
|
if full:
|
||||||
|
correct = failed = False
|
||||||
|
for alg, digest in iteritems(chkmap):
|
||||||
|
other = self._calc_checksum(secret, alg)
|
||||||
|
# NOTE: could do this length check in norm_algs(),
|
||||||
|
# but don't need to be that strict, and want to be able
|
||||||
|
# to parse hashes containing algs not supported by platform.
|
||||||
|
# it's fine if we fail here though.
|
||||||
|
if len(digest) != len(other):
|
||||||
|
raise ValueError("mis-sized %s digest in scram hash: %r != %r"
|
||||||
|
% (alg, len(digest), len(other)))
|
||||||
|
if consteq(other, digest):
|
||||||
|
correct = True
|
||||||
|
else:
|
||||||
|
failed = True
|
||||||
|
if correct and failed:
|
||||||
|
raise ValueError("scram hash verified inconsistently, "
|
||||||
|
"may be corrupted")
|
||||||
|
else:
|
||||||
|
return correct
|
||||||
|
else:
|
||||||
|
# XXX: should this just always use sha1 hash? would be faster.
|
||||||
|
# otherwise only verify against one hash, pick one w/ best security.
|
||||||
|
for alg in self._verify_algs:
|
||||||
|
if alg in chkmap:
|
||||||
|
other = self._calc_checksum(secret, alg)
|
||||||
|
return consteq(other, chkmap[alg])
|
||||||
|
# there should always be sha-1 at the very least,
|
||||||
|
# or something went wrong inside _norm_algs()
|
||||||
|
raise AssertionError("sha-1 digest not found!")
|
||||||
|
|
||||||
|
#===================================================================
|
||||||
|
#
|
||||||
|
#===================================================================
|
||||||
|
|
||||||
|
#=============================================================================
|
||||||
|
# code used for testing scram against protocol examples during development.
|
||||||
|
#=============================================================================
|
||||||
|
##def _test_reference_scram():
|
||||||
|
## "quick hack testing scram reference vectors"
|
||||||
|
## # NOTE: "n,," is GS2 header - see https://tools.ietf.org/html/rfc5801
|
||||||
|
## from passlib.utils.compat import print_
|
||||||
|
##
|
||||||
|
## engine = _scram_engine(
|
||||||
|
## alg="sha-1",
|
||||||
|
## salt='QSXCR+Q6sek8bf92'.decode("base64"),
|
||||||
|
## rounds=4096,
|
||||||
|
## password=u("pencil"),
|
||||||
|
## )
|
||||||
|
## print_(engine.digest.encode("base64").rstrip())
|
||||||
|
##
|
||||||
|
## msg = engine.format_auth_msg(
|
||||||
|
## username="user",
|
||||||
|
## client_nonce = "fyko+d2lbbFgONRv9qkxdawL",
|
||||||
|
## server_nonce = "3rfcNHYJY1ZVvWVs7j",
|
||||||
|
## header='c=biws',
|
||||||
|
## )
|
||||||
|
##
|
||||||
|
## cp = engine.get_encoded_client_proof(msg)
|
||||||
|
## assert cp == "v0X8v3Bz2T0CJGbJQyF0X+HI4Ts=", cp
|
||||||
|
##
|
||||||
|
## ss = engine.get_encoded_server_sig(msg)
|
||||||
|
## assert ss == "rmF9pqV8S7suAoZWja4dJRkFsKQ=", ss
|
||||||
|
##
|
||||||
|
##class _scram_engine(object):
|
||||||
|
## """helper class for verifying scram hash behavior
|
||||||
|
## against SCRAM protocol examples. not officially part of Passlib.
|
||||||
|
##
|
||||||
|
## takes in alg, salt, rounds, and a digest or password.
|
||||||
|
##
|
||||||
|
## can calculate the various keys & messages of the scram protocol.
|
||||||
|
##
|
||||||
|
## """
|
||||||
|
## #=========================================================
|
||||||
|
## # init
|
||||||
|
## #=========================================================
|
||||||
|
##
|
||||||
|
## @classmethod
|
||||||
|
## def from_string(cls, hash, alg):
|
||||||
|
## "create record from scram hash, for given alg"
|
||||||
|
## return cls(alg, *scram.extract_digest_info(hash, alg))
|
||||||
|
##
|
||||||
|
## def __init__(self, alg, salt, rounds, digest=None, password=None):
|
||||||
|
## self.alg = norm_hash_name(alg)
|
||||||
|
## self.salt = salt
|
||||||
|
## self.rounds = rounds
|
||||||
|
## self.password = password
|
||||||
|
## if password:
|
||||||
|
## data = scram.derive_digest(password, salt, rounds, alg)
|
||||||
|
## if digest and data != digest:
|
||||||
|
## raise ValueError("password doesn't match digest")
|
||||||
|
## else:
|
||||||
|
## digest = data
|
||||||
|
## elif not digest:
|
||||||
|
## raise TypeError("must provide password or digest")
|
||||||
|
## self.digest = digest
|
||||||
|
##
|
||||||
|
## #=========================================================
|
||||||
|
## # frontend methods
|
||||||
|
## #=========================================================
|
||||||
|
## def get_hash(self, data):
|
||||||
|
## "return hash of raw data"
|
||||||
|
## return hashlib.new(iana_to_hashlib(self.alg), data).digest()
|
||||||
|
##
|
||||||
|
## def get_client_proof(self, msg):
|
||||||
|
## "return client proof of specified auth msg text"
|
||||||
|
## return xor_bytes(self.client_key, self.get_client_sig(msg))
|
||||||
|
##
|
||||||
|
## def get_encoded_client_proof(self, msg):
|
||||||
|
## return self.get_client_proof(msg).encode("base64").rstrip()
|
||||||
|
##
|
||||||
|
## def get_client_sig(self, msg):
|
||||||
|
## "return client signature of specified auth msg text"
|
||||||
|
## return self.get_hmac(self.stored_key, msg)
|
||||||
|
##
|
||||||
|
## def get_server_sig(self, msg):
|
||||||
|
## "return server signature of specified auth msg text"
|
||||||
|
## return self.get_hmac(self.server_key, msg)
|
||||||
|
##
|
||||||
|
## def get_encoded_server_sig(self, msg):
|
||||||
|
## return self.get_server_sig(msg).encode("base64").rstrip()
|
||||||
|
##
|
||||||
|
## def format_server_response(self, client_nonce, server_nonce):
|
||||||
|
## return 'r={client_nonce}{server_nonce},s={salt},i={rounds}'.format(
|
||||||
|
## client_nonce=client_nonce,
|
||||||
|
## server_nonce=server_nonce,
|
||||||
|
## rounds=self.rounds,
|
||||||
|
## salt=self.encoded_salt,
|
||||||
|
## )
|
||||||
|
##
|
||||||
|
## def format_auth_msg(self, username, client_nonce, server_nonce,
|
||||||
|
## header='c=biws'):
|
||||||
|
## return (
|
||||||
|
## 'n={username},r={client_nonce}'
|
||||||
|
## ','
|
||||||
|
## 'r={client_nonce}{server_nonce},s={salt},i={rounds}'
|
||||||
|
## ','
|
||||||
|
## '{header},r={client_nonce}{server_nonce}'
|
||||||
|
## ).format(
|
||||||
|
## username=username,
|
||||||
|
## client_nonce=client_nonce,
|
||||||
|
## server_nonce=server_nonce,
|
||||||
|
## salt=self.encoded_salt,
|
||||||
|
## rounds=self.rounds,
|
||||||
|
## header=header,
|
||||||
|
## )
|
||||||
|
##
|
||||||
|
## #=========================================================
|
||||||
|
## # helpers to calculate & cache constant data
|
||||||
|
## #=========================================================
|
||||||
|
## def _calc_get_hmac(self):
|
||||||
|
## return get_prf("hmac-" + iana_to_hashlib(self.alg))[0]
|
||||||
|
##
|
||||||
|
## def _calc_client_key(self):
|
||||||
|
## return self.get_hmac(self.digest, b("Client Key"))
|
||||||
|
##
|
||||||
|
## def _calc_stored_key(self):
|
||||||
|
## return self.get_hash(self.client_key)
|
||||||
|
##
|
||||||
|
## def _calc_server_key(self):
|
||||||
|
## return self.get_hmac(self.digest, b("Server Key"))
|
||||||
|
##
|
||||||
|
## def _calc_encoded_salt(self):
|
||||||
|
## return self.salt.encode("base64").rstrip()
|
||||||
|
##
|
||||||
|
## #=========================================================
|
||||||
|
## # hacks for calculated attributes
|
||||||
|
## #=========================================================
|
||||||
|
##
|
||||||
|
## def __getattr__(self, attr):
|
||||||
|
## if not attr.startswith("_"):
|
||||||
|
## f = getattr(self, "_calc_" + attr, None)
|
||||||
|
## if f:
|
||||||
|
## value = f()
|
||||||
|
## setattr(self, attr, value)
|
||||||
|
## return value
|
||||||
|
## raise AttributeError("attribute not found")
|
||||||
|
##
|
||||||
|
## def __dir__(self):
|
||||||
|
## cdir = dir(self.__class__)
|
||||||
|
## attrs = set(cdir)
|
||||||
|
## attrs.update(self.__dict__)
|
||||||
|
## attrs.update(attr[6:] for attr in cdir
|
||||||
|
## if attr.startswith("_calc_"))
|
||||||
|
## return sorted(attrs)
|
||||||
|
## #=========================================================
|
||||||
|
## # eoc
|
||||||
|
## #=========================================================
|
||||||
|
|
||||||
|
#=============================================================================
|
||||||
|
# eof
|
||||||
|
#=============================================================================
|
||||||
383
venv/Lib/site-packages/passlib/handlers/scrypt.py
Normal file
383
venv/Lib/site-packages/passlib/handlers/scrypt.py
Normal file
@@ -0,0 +1,383 @@
|
|||||||
|
"""passlib.handlers.scrypt -- scrypt password hash"""
|
||||||
|
#=============================================================================
|
||||||
|
# imports
|
||||||
|
#=============================================================================
|
||||||
|
from __future__ import with_statement, absolute_import
|
||||||
|
# core
|
||||||
|
import logging; log = logging.getLogger(__name__)
|
||||||
|
# site
|
||||||
|
# pkg
|
||||||
|
from passlib.crypto import scrypt as _scrypt
|
||||||
|
from passlib.utils import h64, to_bytes
|
||||||
|
from passlib.utils.binary import h64, b64s_decode, b64s_encode
|
||||||
|
from passlib.utils.compat import u, bascii_to_str, suppress_cause
|
||||||
|
from passlib.utils.decor import classproperty
|
||||||
|
import passlib.utils.handlers as uh
|
||||||
|
# local
|
||||||
|
__all__ = [
|
||||||
|
"scrypt",
|
||||||
|
]
|
||||||
|
|
||||||
|
#=============================================================================
|
||||||
|
# scrypt format identifiers
|
||||||
|
#=============================================================================
|
||||||
|
|
||||||
|
IDENT_SCRYPT = u("$scrypt$") # identifier used by passlib
|
||||||
|
IDENT_7 = u("$7$") # used by official scrypt spec
|
||||||
|
|
||||||
|
_UDOLLAR = u("$")
|
||||||
|
|
||||||
|
#=============================================================================
|
||||||
|
# handler
|
||||||
|
#=============================================================================
|
||||||
|
class scrypt(uh.ParallelismMixin, uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.HasManyIdents,
|
||||||
|
uh.GenericHandler):
|
||||||
|
"""This class implements an SCrypt-based password [#scrypt-home]_ hash, and follows the :ref:`password-hash-api`.
|
||||||
|
|
||||||
|
It supports a variable-length salt, a variable number of rounds,
|
||||||
|
as well as some custom tuning parameters unique to scrypt (see below).
|
||||||
|
|
||||||
|
The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
|
||||||
|
|
||||||
|
:type salt: str
|
||||||
|
:param salt:
|
||||||
|
Optional salt string.
|
||||||
|
If specified, the length must be between 0-1024 bytes.
|
||||||
|
If not specified, one will be auto-generated (this is recommended).
|
||||||
|
|
||||||
|
:type salt_size: int
|
||||||
|
:param salt_size:
|
||||||
|
Optional number of bytes to use when autogenerating new salts.
|
||||||
|
Defaults to 16 bytes, but can be any value between 0 and 1024.
|
||||||
|
|
||||||
|
:type rounds: int
|
||||||
|
:param rounds:
|
||||||
|
Optional number of rounds to use.
|
||||||
|
Defaults to 16, but must be within ``range(1,32)``.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
Unlike many hash algorithms, increasing the rounds value
|
||||||
|
will increase both the time *and memory* required to hash a password.
|
||||||
|
|
||||||
|
:type block_size: int
|
||||||
|
:param block_size:
|
||||||
|
Optional block size to pass to scrypt hash function (the ``r`` parameter).
|
||||||
|
Useful for tuning scrypt to optimal performance for your CPU architecture.
|
||||||
|
Defaults to 8.
|
||||||
|
|
||||||
|
:type parallelism: int
|
||||||
|
:param parallelism:
|
||||||
|
Optional parallelism to pass to scrypt hash function (the ``p`` parameter).
|
||||||
|
Defaults to 1.
|
||||||
|
|
||||||
|
:type relaxed: bool
|
||||||
|
:param relaxed:
|
||||||
|
By default, providing an invalid value for one of the other
|
||||||
|
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
|
||||||
|
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
|
||||||
|
will be issued instead. Correctable errors include ``rounds``
|
||||||
|
that are too small or too large, and ``salt`` strings that are too long.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The underlying scrypt hash function has a number of limitations
|
||||||
|
on it's parameter values, which forbids certain combinations of settings.
|
||||||
|
The requirements are:
|
||||||
|
|
||||||
|
* ``linear_rounds = 2**<some positive integer>``
|
||||||
|
* ``linear_rounds < 2**(16 * block_size)``
|
||||||
|
* ``block_size * parallelism <= 2**30-1``
|
||||||
|
|
||||||
|
.. todo::
|
||||||
|
|
||||||
|
This class currently does not support configuring default values
|
||||||
|
for ``block_size`` or ``parallelism`` via a :class:`~passlib.context.CryptContext`
|
||||||
|
configuration.
|
||||||
|
"""
|
||||||
|
|
||||||
|
#===================================================================
|
||||||
|
# class attrs
|
||||||
|
#===================================================================
|
||||||
|
|
||||||
|
#------------------------
|
||||||
|
# PasswordHash
|
||||||
|
#------------------------
|
||||||
|
name = "scrypt"
|
||||||
|
setting_kwds = ("ident", "salt", "salt_size", "rounds", "block_size", "parallelism")
|
||||||
|
|
||||||
|
#------------------------
|
||||||
|
# GenericHandler
|
||||||
|
#------------------------
|
||||||
|
# NOTE: scrypt supports arbitrary output sizes. since it's output runs through
|
||||||
|
# pbkdf2-hmac-sha256 before returning, and this could be raised eventually...
|
||||||
|
# but a 256-bit digest is more than sufficient for password hashing.
|
||||||
|
# XXX: make checksum size configurable? could merge w/ argon2 code that does this.
|
||||||
|
checksum_size = 32
|
||||||
|
|
||||||
|
#------------------------
|
||||||
|
# HasManyIdents
|
||||||
|
#------------------------
|
||||||
|
default_ident = IDENT_SCRYPT
|
||||||
|
ident_values = (IDENT_SCRYPT, IDENT_7)
|
||||||
|
|
||||||
|
#------------------------
|
||||||
|
# HasRawSalt
|
||||||
|
#------------------------
|
||||||
|
default_salt_size = 16
|
||||||
|
max_salt_size = 1024
|
||||||
|
|
||||||
|
#------------------------
|
||||||
|
# HasRounds
|
||||||
|
#------------------------
|
||||||
|
# TODO: would like to dynamically pick this based on system
|
||||||
|
default_rounds = 16
|
||||||
|
min_rounds = 1
|
||||||
|
max_rounds = 31 # limited by scrypt alg
|
||||||
|
rounds_cost = "log2"
|
||||||
|
|
||||||
|
# TODO: make default block size configurable via using(), and deprecatable via .needs_update()
|
||||||
|
|
||||||
|
#===================================================================
|
||||||
|
# instance attrs
|
||||||
|
#===================================================================
|
||||||
|
|
||||||
|
#: default parallelism setting (min=1 currently hardcoded in mixin)
|
||||||
|
parallelism = 1
|
||||||
|
|
||||||
|
#: default block size setting
|
||||||
|
block_size = 8
|
||||||
|
|
||||||
|
#===================================================================
|
||||||
|
# variant constructor
|
||||||
|
#===================================================================
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def using(cls, block_size=None, **kwds):
|
||||||
|
subcls = super(scrypt, cls).using(**kwds)
|
||||||
|
if block_size is not None:
|
||||||
|
if isinstance(block_size, uh.native_string_types):
|
||||||
|
block_size = int(block_size)
|
||||||
|
subcls.block_size = subcls._norm_block_size(block_size, relaxed=kwds.get("relaxed"))
|
||||||
|
|
||||||
|
# make sure param combination is valid for scrypt()
|
||||||
|
try:
|
||||||
|
_scrypt.validate(1 << cls.default_rounds, cls.block_size, cls.parallelism)
|
||||||
|
except ValueError as err:
|
||||||
|
raise suppress_cause(ValueError("scrypt: invalid settings combination: " + str(err)))
|
||||||
|
|
||||||
|
return subcls
|
||||||
|
|
||||||
|
#===================================================================
|
||||||
|
# parsing
|
||||||
|
#===================================================================
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_string(cls, hash):
|
||||||
|
return cls(**cls.parse(hash))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse(cls, hash):
|
||||||
|
ident, suffix = cls._parse_ident(hash)
|
||||||
|
func = getattr(cls, "_parse_%s_string" % ident.strip(_UDOLLAR), None)
|
||||||
|
if func:
|
||||||
|
return func(suffix)
|
||||||
|
else:
|
||||||
|
raise uh.exc.InvalidHashError(cls)
|
||||||
|
|
||||||
|
#
|
||||||
|
# passlib's format:
|
||||||
|
# $scrypt$ln=<logN>,r=<r>,p=<p>$<salt>[$<digest>]
|
||||||
|
# where:
|
||||||
|
# logN, r, p -- decimal-encoded positive integer, no zero-padding
|
||||||
|
# logN -- log cost setting
|
||||||
|
# r -- block size setting (usually 8)
|
||||||
|
# p -- parallelism setting (usually 1)
|
||||||
|
# salt, digest -- b64-nopad encoded bytes
|
||||||
|
#
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _parse_scrypt_string(cls, suffix):
|
||||||
|
# break params, salt, and digest sections
|
||||||
|
parts = suffix.split("$")
|
||||||
|
if len(parts) == 3:
|
||||||
|
params, salt, digest = parts
|
||||||
|
elif len(parts) == 2:
|
||||||
|
params, salt = parts
|
||||||
|
digest = None
|
||||||
|
else:
|
||||||
|
raise uh.exc.MalformedHashError(cls, "malformed hash")
|
||||||
|
|
||||||
|
# break params apart
|
||||||
|
parts = params.split(",")
|
||||||
|
if len(parts) == 3:
|
||||||
|
nstr, bstr, pstr = parts
|
||||||
|
assert nstr.startswith("ln=")
|
||||||
|
assert bstr.startswith("r=")
|
||||||
|
assert pstr.startswith("p=")
|
||||||
|
else:
|
||||||
|
raise uh.exc.MalformedHashError(cls, "malformed settings field")
|
||||||
|
|
||||||
|
return dict(
|
||||||
|
ident=IDENT_SCRYPT,
|
||||||
|
rounds=int(nstr[3:]),
|
||||||
|
block_size=int(bstr[2:]),
|
||||||
|
parallelism=int(pstr[2:]),
|
||||||
|
salt=b64s_decode(salt.encode("ascii")),
|
||||||
|
checksum=b64s_decode(digest.encode("ascii")) if digest else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
#
|
||||||
|
# official format specification defined at
|
||||||
|
# https://gitlab.com/jas/scrypt-unix-crypt/blob/master/unix-scrypt.txt
|
||||||
|
# format:
|
||||||
|
# $7$<N><rrrrr><ppppp><salt...>[$<digest>]
|
||||||
|
# 0 12345 67890 1
|
||||||
|
# where:
|
||||||
|
# All bytes use h64-little-endian encoding
|
||||||
|
# N: 6-bit log cost setting
|
||||||
|
# r: 30-bit block size setting
|
||||||
|
# p: 30-bit parallelism setting
|
||||||
|
# salt: variable length salt bytes
|
||||||
|
# digest: fixed 32-byte digest
|
||||||
|
#
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _parse_7_string(cls, suffix):
|
||||||
|
# XXX: annoyingly, official spec embeds salt *raw*, yet doesn't specify a hash encoding.
|
||||||
|
# so assuming only h64 chars are valid for salt, and are ASCII encoded.
|
||||||
|
|
||||||
|
# split into params & digest
|
||||||
|
parts = suffix.encode("ascii").split(b"$")
|
||||||
|
if len(parts) == 2:
|
||||||
|
params, digest = parts
|
||||||
|
elif len(parts) == 1:
|
||||||
|
params, = parts
|
||||||
|
digest = None
|
||||||
|
else:
|
||||||
|
raise uh.exc.MalformedHashError()
|
||||||
|
|
||||||
|
# parse params & return
|
||||||
|
if len(params) < 11:
|
||||||
|
raise uh.exc.MalformedHashError(cls, "params field too short")
|
||||||
|
return dict(
|
||||||
|
ident=IDENT_7,
|
||||||
|
rounds=h64.decode_int6(params[:1]),
|
||||||
|
block_size=h64.decode_int30(params[1:6]),
|
||||||
|
parallelism=h64.decode_int30(params[6:11]),
|
||||||
|
salt=params[11:],
|
||||||
|
checksum=h64.decode_bytes(digest) if digest else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
#===================================================================
|
||||||
|
# formatting
|
||||||
|
#===================================================================
|
||||||
|
def to_string(self):
|
||||||
|
ident = self.ident
|
||||||
|
if ident == IDENT_SCRYPT:
|
||||||
|
return "$scrypt$ln=%d,r=%d,p=%d$%s$%s" % (
|
||||||
|
self.rounds,
|
||||||
|
self.block_size,
|
||||||
|
self.parallelism,
|
||||||
|
bascii_to_str(b64s_encode(self.salt)),
|
||||||
|
bascii_to_str(b64s_encode(self.checksum)),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
assert ident == IDENT_7
|
||||||
|
salt = self.salt
|
||||||
|
try:
|
||||||
|
salt.decode("ascii")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
raise suppress_cause(NotImplementedError("scrypt $7$ hashes dont support non-ascii salts"))
|
||||||
|
return bascii_to_str(b"".join([
|
||||||
|
b"$7$",
|
||||||
|
h64.encode_int6(self.rounds),
|
||||||
|
h64.encode_int30(self.block_size),
|
||||||
|
h64.encode_int30(self.parallelism),
|
||||||
|
self.salt,
|
||||||
|
b"$",
|
||||||
|
h64.encode_bytes(self.checksum)
|
||||||
|
]))
|
||||||
|
|
||||||
|
#===================================================================
|
||||||
|
# init
|
||||||
|
#===================================================================
|
||||||
|
def __init__(self, block_size=None, **kwds):
|
||||||
|
super(scrypt, self).__init__(**kwds)
|
||||||
|
|
||||||
|
# init block size
|
||||||
|
if block_size is None:
|
||||||
|
assert uh.validate_default_value(self, self.block_size, self._norm_block_size,
|
||||||
|
param="block_size")
|
||||||
|
else:
|
||||||
|
self.block_size = self._norm_block_size(block_size)
|
||||||
|
|
||||||
|
# NOTE: if hash contains invalid complex constraint, relying on error
|
||||||
|
# being raised by scrypt call in _calc_checksum()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _norm_block_size(cls, block_size, relaxed=False):
|
||||||
|
return uh.norm_integer(cls, block_size, min=1, param="block_size", relaxed=relaxed)
|
||||||
|
|
||||||
|
def _generate_salt(self):
|
||||||
|
salt = super(scrypt, self)._generate_salt()
|
||||||
|
if self.ident == IDENT_7:
|
||||||
|
# this format doesn't support non-ascii salts.
|
||||||
|
# as workaround, we take raw bytes, encoded to base64
|
||||||
|
salt = b64s_encode(salt)
|
||||||
|
return salt
|
||||||
|
|
||||||
|
#===================================================================
|
||||||
|
# backend configuration
|
||||||
|
# NOTE: this following HasManyBackends' API, but provides it's own implementation,
|
||||||
|
# which actually switches the backend that 'passlib.crypto.scrypt.scrypt()' uses.
|
||||||
|
#===================================================================
|
||||||
|
|
||||||
|
@classproperty
|
||||||
|
def backends(cls):
|
||||||
|
return _scrypt.backend_values
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_backend(cls):
|
||||||
|
return _scrypt.backend
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def has_backend(cls, name="any"):
|
||||||
|
try:
|
||||||
|
cls.set_backend(name, dryrun=True)
|
||||||
|
return True
|
||||||
|
except uh.exc.MissingBackendError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def set_backend(cls, name="any", dryrun=False):
|
||||||
|
_scrypt._set_backend(name, dryrun=dryrun)
|
||||||
|
|
||||||
|
#===================================================================
|
||||||
|
# digest calculation
|
||||||
|
#===================================================================
|
||||||
|
def _calc_checksum(self, secret):
|
||||||
|
secret = to_bytes(secret, param="secret")
|
||||||
|
return _scrypt.scrypt(secret, self.salt, n=(1 << self.rounds), r=self.block_size,
|
||||||
|
p=self.parallelism, keylen=self.checksum_size)
|
||||||
|
|
||||||
|
#===================================================================
|
||||||
|
# hash migration
|
||||||
|
#===================================================================
|
||||||
|
|
||||||
|
def _calc_needs_update(self, **kwds):
|
||||||
|
"""
|
||||||
|
mark hash as needing update if rounds is outside desired bounds.
|
||||||
|
"""
|
||||||
|
# XXX: for now, marking all hashes which don't have matching block_size setting
|
||||||
|
if self.block_size != type(self).block_size:
|
||||||
|
return True
|
||||||
|
return super(scrypt, self)._calc_needs_update(**kwds)
|
||||||
|
|
||||||
|
#===================================================================
|
||||||
|
# eoc
|
||||||
|
#===================================================================
|
||||||
|
|
||||||
|
#=============================================================================
|
||||||
|
# eof
|
||||||
|
#=============================================================================
|
||||||
Reference in New Issue
Block a user