mirror of
https://github.com/GAM-team/GAM.git
synced 2025-05-12 20:27:20 +00:00
458 lines
18 KiB
Python
458 lines
18 KiB
Python
"""passlib.bcrypt -- implementation of OpenBSD's BCrypt algorithm.
|
|
|
|
TODO:
|
|
|
|
* support 2x and altered-2a hashes?
|
|
http://www.openwall.com/lists/oss-security/2011/06/27/9
|
|
|
|
* deal with lack of PY3-compatibile c-ext implementation
|
|
"""
|
|
#=============================================================================
|
|
# imports
|
|
#=============================================================================
|
|
from __future__ import with_statement, absolute_import
|
|
# core
|
|
from base64 import b64encode
|
|
from hashlib import sha256
|
|
import os
|
|
import re
|
|
import logging; log = logging.getLogger(__name__)
|
|
from warnings import warn
|
|
# site
|
|
try:
|
|
import bcrypt as _bcrypt
|
|
except ImportError: # pragma: no cover
|
|
_bcrypt = None
|
|
try:
|
|
from bcryptor.engine import Engine as bcryptor_engine
|
|
except ImportError: # pragma: no cover
|
|
bcryptor_engine = None
|
|
# pkg
|
|
from passlib.exc import PasslibHashWarning
|
|
from passlib.utils import bcrypt64, safe_crypt, repeat_string, to_bytes, \
|
|
classproperty, rng, getrandstr, test_crypt, to_unicode
|
|
from passlib.utils.compat import bytes, b, u, uascii_to_str, unicode, str_to_uascii
|
|
import passlib.utils.handlers as uh
|
|
|
|
# local
|
|
__all__ = [
|
|
"bcrypt",
|
|
]
|
|
|
|
#=============================================================================
|
|
# support funcs & constants
|
|
#=============================================================================
|
|
_builtin_bcrypt = None
|
|
|
|
def _load_builtin():
|
|
global _builtin_bcrypt
|
|
if _builtin_bcrypt is None:
|
|
from passlib.utils._blowfish import raw_bcrypt as _builtin_bcrypt
|
|
|
|
IDENT_2 = u("$2$")
|
|
IDENT_2A = u("$2a$")
|
|
IDENT_2X = u("$2x$")
|
|
IDENT_2Y = u("$2y$")
|
|
_BNULL = b('\x00')
|
|
|
|
#=============================================================================
|
|
# handler
|
|
#=============================================================================
|
|
class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh.GenericHandler):
|
|
"""This class implements the BCrypt password hash, and follows the :ref:`password-hash-api`.
|
|
|
|
It supports a fixed-length salt, and a variable number of rounds.
|
|
|
|
The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
|
|
|
|
:type salt: str
|
|
:param salt:
|
|
Optional salt string.
|
|
If not specified, one will be autogenerated (this is recommended).
|
|
If specified, it must be 22 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
|
|
|
|
:type rounds: int
|
|
:param rounds:
|
|
Optional number of rounds to use.
|
|
Defaults to 12, must be between 4 and 31, inclusive.
|
|
This value is logarithmic, the actual number of iterations used will be :samp:`2**{rounds}`
|
|
-- increasing the rounds by +1 will double the amount of time taken.
|
|
|
|
:type ident: str
|
|
:param ident:
|
|
Specifies which version of the BCrypt algorithm will be used when creating a new hash.
|
|
Typically this option is not needed, as the default (``"2a"``) is usually the correct choice.
|
|
If specified, it must be one of the following:
|
|
|
|
* ``"2"`` - the first revision of BCrypt, which suffers from a minor security flaw and is generally not used anymore.
|
|
* ``"2a"`` - latest revision of the official BCrypt algorithm, and the current default.
|
|
* ``"2y"`` - format specific to the *crypt_blowfish* BCrypt implementation,
|
|
identical to ``"2a"`` in all but name.
|
|
|
|
:type relaxed: bool
|
|
:param relaxed:
|
|
By default, providing an invalid value for one of the other
|
|
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
|
|
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
|
|
will be issued instead. Correctable errors include ``rounds``
|
|
that are too small or too large, and ``salt`` strings that are too long.
|
|
|
|
.. versionadded:: 1.6
|
|
|
|
.. versionchanged:: 1.6
|
|
This class now supports ``"2y"`` hashes, and recognizes
|
|
(but does not support) the broken ``"2x"`` hashes.
|
|
(see the :ref:`crypt_blowfish bug <crypt-blowfish-bug>`
|
|
for details).
|
|
|
|
.. versionchanged:: 1.6
|
|
Added a pure-python backend.
|
|
"""
|
|
|
|
#===================================================================
|
|
# class attrs
|
|
#===================================================================
|
|
#--GenericHandler--
|
|
name = "bcrypt"
|
|
setting_kwds = ("salt", "rounds", "ident")
|
|
checksum_size = 31
|
|
checksum_chars = bcrypt64.charmap
|
|
|
|
#--HasManyIdents--
|
|
default_ident = IDENT_2A
|
|
ident_values = (IDENT_2, IDENT_2A, IDENT_2X, IDENT_2Y)
|
|
ident_aliases = {u("2"): IDENT_2, u("2a"): IDENT_2A, u("2y"): IDENT_2Y}
|
|
|
|
#--HasSalt--
|
|
min_salt_size = max_salt_size = 22
|
|
salt_chars = bcrypt64.charmap
|
|
# NOTE: 22nd salt char must be in bcrypt64._padinfo2[1], not full charmap
|
|
|
|
#--HasRounds--
|
|
default_rounds = 12 # current passlib default
|
|
min_rounds = 4 # minimum from bcrypt specification
|
|
max_rounds = 31 # 32-bit integer limit (since real_rounds=1<<rounds)
|
|
rounds_cost = "log2"
|
|
|
|
#===================================================================
|
|
# formatting
|
|
#===================================================================
|
|
|
|
@classmethod
|
|
def from_string(cls, hash):
|
|
ident, tail = cls._parse_ident(hash)
|
|
if ident == IDENT_2X:
|
|
raise ValueError("crypt_blowfish's buggy '2x' hashes are not "
|
|
"currently supported")
|
|
rounds_str, data = tail.split(u("$"))
|
|
rounds = int(rounds_str)
|
|
if rounds_str != u('%02d') % (rounds,):
|
|
raise uh.exc.MalformedHashError(cls, "malformed cost field")
|
|
salt, chk = data[:22], data[22:]
|
|
return cls(
|
|
rounds=rounds,
|
|
salt=salt,
|
|
checksum=chk or None,
|
|
ident=ident,
|
|
)
|
|
|
|
def to_string(self):
|
|
hash = u("%s%02d$%s%s") % (self.ident, self.rounds, self.salt,
|
|
self.checksum or u(''))
|
|
return uascii_to_str(hash)
|
|
|
|
def _get_config(self, ident=None):
|
|
"internal helper to prepare config string for backends"
|
|
if ident is None:
|
|
ident = self.ident
|
|
if ident == IDENT_2Y:
|
|
# none of passlib's backends suffered from crypt_blowfish's
|
|
# buggy "2a" hash, which means we can safely implement
|
|
# crypt_blowfish's "2y" hash by passing "2a" to the backends.
|
|
ident = IDENT_2A
|
|
else:
|
|
# no backends currently support 2x, but that should have
|
|
# been caught earlier in from_string()
|
|
assert ident != IDENT_2X
|
|
config = u("%s%02d$%s") % (ident, self.rounds, self.salt)
|
|
return uascii_to_str(config)
|
|
|
|
#===================================================================
|
|
# specialized salt generation - fixes passlib issue 25
|
|
#===================================================================
|
|
|
|
@classmethod
|
|
def _bind_needs_update(cls, **settings):
|
|
return cls._needs_update
|
|
|
|
@classmethod
|
|
def _needs_update(cls, hash, secret):
|
|
if isinstance(hash, bytes):
|
|
hash = hash.decode("ascii")
|
|
# check for incorrect padding bits (passlib issue 25)
|
|
if hash.startswith(IDENT_2A) and hash[28] not in bcrypt64._padinfo2[1]:
|
|
return True
|
|
# TODO: try to detect incorrect $2x$ hashes using *secret*
|
|
return False
|
|
|
|
@classmethod
|
|
def normhash(cls, hash):
|
|
"helper to normalize hash, correcting any bcrypt padding bits"
|
|
if cls.identify(hash):
|
|
return cls.from_string(hash).to_string()
|
|
else:
|
|
return hash
|
|
|
|
def _generate_salt(self, salt_size):
|
|
# generate random salt as normal,
|
|
# but repair last char so the padding bits always decode to zero.
|
|
salt = super(bcrypt, self)._generate_salt(salt_size)
|
|
return bcrypt64.repair_unused(salt)
|
|
|
|
def _norm_salt(self, salt, **kwds):
|
|
salt = super(bcrypt, self)._norm_salt(salt, **kwds)
|
|
assert salt is not None, "HasSalt didn't generate new salt!"
|
|
changed, salt = bcrypt64.check_repair_unused(salt)
|
|
if changed:
|
|
# FIXME: if salt was provided by user, this message won't be
|
|
# correct. not sure if we want to throw error, or use different warning.
|
|
warn(
|
|
"encountered a bcrypt salt with incorrectly set padding bits; "
|
|
"you may want to use bcrypt.normhash() "
|
|
"to fix this; see Passlib 1.5.3 changelog.",
|
|
PasslibHashWarning)
|
|
return salt
|
|
|
|
def _norm_checksum(self, checksum):
|
|
checksum = super(bcrypt, self)._norm_checksum(checksum)
|
|
if not checksum:
|
|
return None
|
|
changed, checksum = bcrypt64.check_repair_unused(checksum)
|
|
if changed:
|
|
warn(
|
|
"encountered a bcrypt hash with incorrectly set padding bits; "
|
|
"you may want to use bcrypt.normhash() "
|
|
"to fix this; see Passlib 1.5.3 changelog.",
|
|
PasslibHashWarning)
|
|
return checksum
|
|
|
|
#===================================================================
|
|
# primary interface
|
|
#===================================================================
|
|
backends = ("bcrypt", "pybcrypt", "bcryptor", "os_crypt", "builtin")
|
|
|
|
@classproperty
|
|
def _has_backend_bcrypt(cls):
|
|
return _bcrypt is not None and hasattr(_bcrypt, "_ffi")
|
|
|
|
@classproperty
|
|
def _has_backend_pybcrypt(cls):
|
|
return _bcrypt is not None and not hasattr(_bcrypt, "_ffi")
|
|
|
|
@classproperty
|
|
def _has_backend_bcryptor(cls):
|
|
return bcryptor_engine is not None
|
|
|
|
@classproperty
|
|
def _has_backend_builtin(cls):
|
|
if os.environ.get("PASSLIB_BUILTIN_BCRYPT") not in ["enable","enabled"]:
|
|
return False
|
|
# look at it cross-eyed, and it loads itself
|
|
_load_builtin()
|
|
return True
|
|
|
|
@classproperty
|
|
def _has_backend_os_crypt(cls):
|
|
# XXX: what to do if "2" isn't supported, but "2a" is?
|
|
# "2" is *very* rare, and can fake it using "2a"+repeat_string
|
|
h1 = '$2$04$......................1O4gOrCYaqBG3o/4LnT2ykQUt1wbyju'
|
|
h2 = '$2a$04$......................qiOQjkB8hxU8OzRhS.GhRMa4VUnkPty'
|
|
return test_crypt("test",h1) and test_crypt("test", h2)
|
|
|
|
@classmethod
|
|
def _no_backends_msg(cls):
|
|
return "no bcrypt backends available - please install py-bcrypt"
|
|
|
|
def _calc_checksum(self, secret):
|
|
"common backend code"
|
|
if isinstance(secret, unicode):
|
|
secret = secret.encode("utf-8")
|
|
if _BNULL in secret:
|
|
# NOTE: especially important to forbid NULLs for bcrypt, since many
|
|
# backends (bcryptor, bcrypt) happily accept them, and then
|
|
# silently truncate the password at first NULL they encounter!
|
|
raise uh.exc.NullPasswordError(self)
|
|
return self._calc_checksum_backend(secret)
|
|
|
|
def _calc_checksum_os_crypt(self, secret):
|
|
config = self._get_config()
|
|
hash = safe_crypt(secret, config)
|
|
if hash:
|
|
assert hash.startswith(config) and len(hash) == len(config)+31
|
|
return hash[-31:]
|
|
else:
|
|
# NOTE: it's unlikely any other backend will be available,
|
|
# but checking before we bail, just in case.
|
|
for name in self.backends:
|
|
if name != "os_crypt" and self.has_backend(name):
|
|
func = getattr(self, "_calc_checksum_" + name)
|
|
return func(secret)
|
|
raise uh.exc.MissingBackendError(
|
|
"password can't be handled by os_crypt, "
|
|
"recommend installing py-bcrypt.",
|
|
)
|
|
|
|
def _calc_checksum_bcrypt(self, secret):
|
|
# bcrypt behavior:
|
|
# hash must be ascii bytes
|
|
# secret must be bytes
|
|
# returns bytes
|
|
if self.ident == IDENT_2:
|
|
# bcrypt doesn't support $2$ hashes; but we can fake $2$ behavior
|
|
# using the $2a$ algorithm, by repeating the password until
|
|
# it's at least 72 chars in length.
|
|
if secret:
|
|
secret = repeat_string(secret, 72)
|
|
config = self._get_config(IDENT_2A)
|
|
else:
|
|
config = self._get_config()
|
|
if isinstance(config, unicode):
|
|
config = config.encode("ascii")
|
|
hash = _bcrypt.hashpw(secret, config)
|
|
assert hash.startswith(config) and len(hash) == len(config)+31
|
|
assert isinstance(hash, bytes)
|
|
return hash[-31:].decode("ascii")
|
|
|
|
def _calc_checksum_pybcrypt(self, secret):
|
|
# py-bcrypt behavior:
|
|
# py2: unicode secret/hash encoded as ascii bytes before use,
|
|
# bytes taken as-is; returns ascii bytes.
|
|
# py3: unicode secret encoded as utf-8 bytes,
|
|
# hash encoded as ascii bytes, returns ascii unicode.
|
|
config = self._get_config()
|
|
hash = _bcrypt.hashpw(secret, config)
|
|
assert hash.startswith(config) and len(hash) == len(config)+31
|
|
return str_to_uascii(hash[-31:])
|
|
|
|
def _calc_checksum_bcryptor(self, secret):
|
|
# bcryptor behavior:
|
|
# py2: unicode secret/hash encoded as ascii bytes before use,
|
|
# bytes taken as-is; returns ascii bytes.
|
|
# py3: not supported
|
|
if self.ident == IDENT_2:
|
|
# bcryptor doesn't support $2$ hashes; but we can fake $2$ behavior
|
|
# using the $2a$ algorithm, by repeating the password until
|
|
# it's at least 72 chars in length.
|
|
if secret:
|
|
secret = repeat_string(secret, 72)
|
|
config = self._get_config(IDENT_2A)
|
|
else:
|
|
config = self._get_config()
|
|
hash = bcryptor_engine(False).hash_key(secret, config)
|
|
assert hash.startswith(config) and len(hash) == len(config)+31
|
|
return str_to_uascii(hash[-31:])
|
|
|
|
def _calc_checksum_builtin(self, secret):
|
|
chk = _builtin_bcrypt(secret, self.ident.strip("$"),
|
|
self.salt.encode("ascii"), self.rounds)
|
|
return chk.decode("ascii")
|
|
|
|
#===================================================================
|
|
# eoc
|
|
#===================================================================
|
|
|
|
_UDOLLAR = u("$")
|
|
|
|
class bcrypt_sha256(bcrypt):
|
|
"""This class implements a composition of BCrypt+SHA256, and follows the :ref:`password-hash-api`.
|
|
|
|
It supports a fixed-length salt, and a variable number of rounds.
|
|
|
|
The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept
|
|
all the same optional keywords as the base :class:`bcrypt` hash.
|
|
|
|
.. versionadded:: 1.6.2
|
|
"""
|
|
name = "bcrypt_sha256"
|
|
|
|
# this is locked at 2a for now.
|
|
ident_values = (IDENT_2A,)
|
|
|
|
# sample hash:
|
|
# $bcrypt-sha256$2a,6$/3OeRpbOf8/l6nPPRdZPp.$nRiyYqPobEZGdNRBWihQhiFDh1ws1tu
|
|
# $bcrypt-sha256$ -- prefix/identifier
|
|
# 2a -- bcrypt variant
|
|
# , -- field separator
|
|
# 6 -- bcrypt work factor
|
|
# $ -- section separator
|
|
# /3OeRpbOf8/l6nPPRdZPp. -- salt
|
|
# $ -- section separator
|
|
# nRiyYqPobEZGdNRBWihQhiFDh1ws1tu -- digest
|
|
|
|
# XXX: we can't use .ident attr due to bcrypt code using it.
|
|
# working around that via prefix.
|
|
prefix = u('$bcrypt-sha256$')
|
|
|
|
_hash_re = re.compile(r"""
|
|
^
|
|
[$]bcrypt-sha256
|
|
[$](?P<variant>[a-z0-9]+)
|
|
,(?P<rounds>\d{1,2})
|
|
[$](?P<salt>[^$]{22})
|
|
([$](?P<digest>.{31}))?
|
|
$
|
|
""", re.X)
|
|
|
|
@classmethod
|
|
def identify(cls, hash):
|
|
hash = uh.to_unicode_for_identify(hash)
|
|
if not hash:
|
|
return False
|
|
return hash.startswith(cls.prefix)
|
|
|
|
@classmethod
|
|
def from_string(cls, hash):
|
|
hash = to_unicode(hash, "ascii", "hash")
|
|
if not hash.startswith(cls.prefix):
|
|
raise uh.exc.InvalidHashError(cls)
|
|
m = cls._hash_re.match(hash)
|
|
if not m:
|
|
raise uh.exc.MalformedHashError(cls)
|
|
rounds = m.group("rounds")
|
|
if rounds.startswith(uh._UZERO) and rounds != uh._UZERO:
|
|
raise uh.exc.ZeroPaddedRoundsError(cls)
|
|
return cls(ident=m.group("variant"),
|
|
rounds=int(rounds),
|
|
salt=m.group("salt"),
|
|
checksum=m.group("digest"),
|
|
)
|
|
|
|
def to_string(self):
|
|
hash = u("%s%s,%d$%s") % (self.prefix, self.ident.strip(_UDOLLAR),
|
|
self.rounds, self.salt)
|
|
if self.checksum:
|
|
hash = u("%s$%s") % (hash, self.checksum)
|
|
return uascii_to_str(hash)
|
|
|
|
def _calc_checksum(self, secret):
|
|
# NOTE: this bypasses bcrypt's _calc_checksum,
|
|
# so has to take care of all it's issues, such as secret encoding.
|
|
if isinstance(secret, unicode):
|
|
secret = secret.encode("utf-8")
|
|
# NOTE: can't use digest directly, since bcrypt stops at first NULL.
|
|
# NOTE: bcrypt doesn't fully mix entropy for bytes 55-72 of password
|
|
# (XXX: citation needed), so we don't want key to be > 55 bytes.
|
|
# thus, have to use base64 (44 bytes) rather than hex (64 bytes).
|
|
key = b64encode(sha256(secret).digest())
|
|
return self._calc_checksum_backend(key)
|
|
|
|
# patch set_backend so it modifies bcrypt class, not this one...
|
|
# else it would clobber our _calc_checksum() wrapper above.
|
|
@classmethod
|
|
def set_backend(cls, *args, **kwds):
|
|
return bcrypt.set_backend(*args, **kwds)
|
|
|
|
#=============================================================================
|
|
# eof
|
|
#=============================================================================
|