Merge branch 'master' of github.com:jay0lee/GAM
This commit is contained in:
Jay Lee 2014-06-29 14:20:56 -04:00
commit c52e646c67
74 changed files with 31286 additions and 8 deletions

20
gam.py
View File

@ -1046,6 +1046,10 @@ def doDelegates(users):
if delete_alias:
doDeleteAlias(alias_email=use_delegate_address)
def gen_sha512_hash(password):
from passlib.hash import sha512_crypt
return sha512_crypt(password)
def getDelegates(users):
emailsettings = getEmailSettingsObject()
csv_format = False
@ -3502,10 +3506,10 @@ def doCreateUser():
if need_password:
body[u'password'] = u''.join(random.sample(u'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789~`!@#$%^&*()-=_+:;"\'{}[]\\|', 25))
if need_to_hash_password:
newhash = hashlib.sha1()
newhash.update(body[u'password'])
body[u'password'] = newhash.hexdigest()
body[u'hashFunction'] = u'SHA-1'
#newhash = hashlib.sha1()
#newhash.update(body[u'password'])
body[u'password'] = gen_sha512_hash(body[u'password'])
body[u'hashFunction'] = u'crypt'
print u"Creating account for %s" % body[u'primaryEmail']
callGAPI(service=cd.users(), function='insert', body=body)
if do_admin:
@ -3958,10 +3962,10 @@ def doUpdateUser(users):
print u'Error: didn\'t expect %s command at position %s' % (sys.argv[i], i)
sys.exit(2)
if gotPassword and not (isSHA1 or isMD5 or isCrypt or nohash):
newhash = hashlib.sha1()
newhash.update(body[u'password'])
body[u'password'] = newhash.hexdigest()
body[u'hashFunction'] = u'SHA-1'
#newhash = hashlib.sha1()
#newhash.update(body[u'password'])
body[u'password'] = gen_sha512_hash(body[u'password'])
body[u'hashFunction'] = u'crypt'
for user in users:
if user[:4].lower() == u'uid:':
user = user[4:]

3
passlib/__init__.py Normal file
View File

@ -0,0 +1,3 @@
"""passlib - suite of password hashing & generation routinges"""
__version__ = '1.6.2'

View File

@ -0,0 +1 @@
"""passlib.setup - helpers used by passlib's setup.py script"""

87
passlib/_setup/docdist.py Normal file
View File

@ -0,0 +1,87 @@
"custom command to build doc.zip file"
#=============================================================================
# imports
#=============================================================================
# core
import os
from distutils import dir_util
from distutils.cmd import Command
from distutils.errors import *
from distutils.spawn import spawn
# local
__all__ = [
"docdist"
]
#=============================================================================
# command
#=============================================================================
class docdist(Command):
description = "create zip file containing standalone html docs"
user_options = [
('build-dir=', None, 'Build directory'),
('dist-dir=', 'd',
"directory to put the source distribution archive(s) in "
"[default: dist]"),
('format=', 'f',
"archive format to create (tar, ztar, gztar, zip)"),
('sign', 's', 'sign files using gpg'),
('identity=', 'i', 'GPG identity used to sign files'),
]
def initialize_options(self):
self.build_dir = None
self.dist_dir = None
self.format = None
self.keep_temp = False
self.sign = False
self.identity = None
def finalize_options(self):
if self.identity and not self.sign:
raise DistutilsOptionError(
"Must use --sign for --identity to have meaning"
)
if self.build_dir is None:
cmd = self.get_finalized_command('build')
self.build_dir = os.path.join(cmd.build_base, 'docdist')
if not self.dist_dir:
self.dist_dir = "dist"
if not self.format:
self.format = "zip"
def run(self):
# call build sphinx to build docs
self.run_command("build_sphinx")
cmd = self.get_finalized_command("build_sphinx")
source_dir = cmd.builder_target_dir
# copy to directory with appropriate name
dist = self.distribution
arc_name = "%s-docs-%s" % (dist.get_name(), dist.get_version())
tmp_dir = os.path.join(self.build_dir, arc_name)
if os.path.exists(tmp_dir):
dir_util.remove_tree(tmp_dir, dry_run=self.dry_run)
self.copy_tree(source_dir, tmp_dir, preserve_symlinks=True)
# make archive from dir
arc_base = os.path.join(self.dist_dir, arc_name)
self.arc_filename = self.make_archive(arc_base, self.format,
self.build_dir)
# Sign if requested
if self.sign:
gpg_args = ["gpg", "--detach-sign", "-a", self.arc_filename]
if self.identity:
gpg_args[2:2] = ["--local-user", self.identity]
spawn(gpg_args,
dry_run=self.dry_run)
# cleanup
if not self.keep_temp:
dir_util.remove_tree(tmp_dir, dry_run=self.dry_run)
#=============================================================================
# eof
#=============================================================================

57
passlib/_setup/stamp.py Normal file
View File

@ -0,0 +1,57 @@
"update version string during build"
#=============================================================================
# imports
#=============================================================================
from __future__ import with_statement
# core
import os
import re
import time
from distutils.dist import Distribution
# pkg
# local
__all__ = [
"stamp_source",
"stamp_distutils_output",
]
#=============================================================================
# helpers
#=============================================================================
def get_command_class(opts, name):
return opts['cmdclass'].get(name) or Distribution().get_command_class(name)
def stamp_source(base_dir, version, dry_run=False):
"update version string in passlib dist"
path = os.path.join(base_dir, "passlib", "__init__.py")
with open(path) as fh:
input = fh.read()
output, count = re.subn('(?m)^__version__\s*=.*$',
'__version__ = ' + repr(version),
input)
assert count == 1, "failed to replace version string"
if not dry_run:
os.unlink(path) # sdist likes to use hardlinks
with open(path, "w") as fh:
fh.write(output)
def stamp_distutils_output(opts, version):
# subclass buildpy to update version string in source
_build_py = get_command_class(opts, "build_py")
class build_py(_build_py):
def build_packages(self):
_build_py.build_packages(self)
stamp_source(self.build_lib, version, self.dry_run)
opts['cmdclass']['build_py'] = build_py
# subclass sdist to do same thing
_sdist = get_command_class(opts, "sdist")
class sdist(_sdist):
def make_release_tree(self, base_dir, files):
_sdist.make_release_tree(self, base_dir, files)
stamp_source(base_dir, version, self.dry_run)
opts['cmdclass']['sdist'] = sdist
#=============================================================================
# eof
#=============================================================================

1037
passlib/apache.py Normal file

File diff suppressed because it is too large Load Diff

192
passlib/apps.py Normal file
View File

@ -0,0 +1,192 @@
"""passlib.apps"""
#=============================================================================
# imports
#=============================================================================
# core
import logging; log = logging.getLogger(__name__)
from itertools import chain
# site
# pkg
from passlib import hash
from passlib.context import LazyCryptContext
from passlib.utils import sys_bits
# local
__all__ = [
'custom_app_context',
'django_context',
'ldap_context', 'ldap_nocrypt_context',
'mysql_context', 'mysql4_context', 'mysql3_context',
'phpass_context',
'phpbb3_context',
'postgres_context',
]
#=============================================================================
# master containing all identifiable hashes
#=============================================================================
def _load_master_config():
from passlib.registry import list_crypt_handlers
# get master list
schemes = list_crypt_handlers()
# exclude the ones we know have ambiguous or greedy identify() methods.
excluded = [
# frequently confused for eachother
'bigcrypt',
'crypt16',
# no good identifiers
'cisco_pix',
'cisco_type7',
'htdigest',
'mysql323',
'oracle10',
# all have same size
'lmhash',
'msdcc',
'msdcc2',
'nthash',
# plaintext handlers
'plaintext',
'ldap_plaintext',
# disabled handlers
'django_disabled',
'unix_disabled',
'unix_fallback',
]
for name in excluded:
schemes.remove(name)
# return config
return dict(schemes=schemes, default="sha256_crypt")
master_context = LazyCryptContext(onload=_load_master_config)
#=============================================================================
# for quickly bootstrapping new custom applications
#=============================================================================
custom_app_context = LazyCryptContext(
# choose some reasonbly strong schemes
schemes=["sha512_crypt", "sha256_crypt"],
# set some useful global options
default="sha256_crypt" if sys_bits < 64 else "sha512_crypt",
all__vary_rounds = 0.1,
# set a good starting point for rounds selection
sha512_crypt__min_rounds = 60000,
sha256_crypt__min_rounds = 80000,
# if the admin user category is selected, make a much stronger hash,
admin__sha512_crypt__min_rounds = 120000,
admin__sha256_crypt__min_rounds = 160000,
)
#=============================================================================
# django
#=============================================================================
_django10_schemes = [
"django_salted_sha1", "django_salted_md5", "django_des_crypt",
"hex_md5", "django_disabled",
]
django10_context = LazyCryptContext(
schemes=_django10_schemes,
default="django_salted_sha1",
deprecated=["hex_md5"],
)
_django14_schemes = ["django_pbkdf2_sha256", "django_pbkdf2_sha1",
"django_bcrypt"] + _django10_schemes
django14_context = LazyCryptContext(
schemes=_django14_schemes,
deprecated=_django10_schemes,
)
_django16_schemes = _django14_schemes[:]
_django16_schemes.insert(1, "django_bcrypt_sha256")
django16_context = LazyCryptContext(
schemes=_django16_schemes,
deprecated=_django10_schemes,
)
# this will always point to latest version
django_context = django16_context
#=============================================================================
# ldap
#=============================================================================
std_ldap_schemes = ["ldap_salted_sha1", "ldap_salted_md5",
"ldap_sha1", "ldap_md5",
"ldap_plaintext" ]
# create context with all std ldap schemes EXCEPT crypt
ldap_nocrypt_context = LazyCryptContext(std_ldap_schemes)
# create context with all possible std ldap + ldap crypt schemes
def _iter_ldap_crypt_schemes():
from passlib.utils import unix_crypt_schemes
return ('ldap_' + name for name in unix_crypt_schemes)
def _iter_ldap_schemes():
"helper which iterates over supported std ldap schemes"
return chain(std_ldap_schemes, _iter_ldap_crypt_schemes())
ldap_context = LazyCryptContext(_iter_ldap_schemes())
### create context with all std ldap schemes + crypt schemes for localhost
##def _iter_host_ldap_schemes():
## "helper which iterates over supported std ldap schemes"
## from passlib.handlers.ldap_digests import get_host_ldap_crypt_schemes
## return chain(std_ldap_schemes, get_host_ldap_crypt_schemes())
##ldap_host_context = LazyCryptContext(_iter_host_ldap_schemes())
#=============================================================================
# mysql
#=============================================================================
mysql3_context = LazyCryptContext(["mysql323"])
mysql4_context = LazyCryptContext(["mysql41", "mysql323"], deprecated="mysql323")
mysql_context = mysql4_context # tracks latest mysql version supported
#=============================================================================
# postgres
#=============================================================================
postgres_context = LazyCryptContext(["postgres_md5"])
#=============================================================================
# phpass & variants
#=============================================================================
def _create_phpass_policy(**kwds):
"helper to choose default alg based on bcrypt availability"
kwds['default'] = 'bcrypt' if hash.bcrypt.has_backend() else 'phpass'
return kwds
phpass_context = LazyCryptContext(
schemes=["bcrypt", "phpass", "bsdi_crypt"],
onload=_create_phpass_policy,
)
phpbb3_context = LazyCryptContext(["phpass"], phpass__ident="H")
# TODO: support the drupal phpass variants (see phpass homepage)
#=============================================================================
# roundup
#=============================================================================
_std_roundup_schemes = [ "ldap_hex_sha1", "ldap_hex_md5", "ldap_des_crypt", "roundup_plaintext" ]
roundup10_context = LazyCryptContext(_std_roundup_schemes)
# NOTE: 'roundup15' really applies to roundup 1.4.17+
roundup_context = roundup15_context = LazyCryptContext(
schemes=_std_roundup_schemes + [ "ldap_pbkdf2_sha1" ],
deprecated=_std_roundup_schemes,
default = "ldap_pbkdf2_sha1",
ldap_pbkdf2_sha1__default_rounds = 10000,
)
#=============================================================================
# eof
#=============================================================================

2711
passlib/context.py Normal file

File diff suppressed because it is too large Load Diff

184
passlib/exc.py Normal file
View File

@ -0,0 +1,184 @@
"""passlib.exc -- exceptions & warnings raised by passlib"""
#=============================================================================
# exceptions
#=============================================================================
class MissingBackendError(RuntimeError):
"""Error raised if multi-backend handler has no available backends;
or if specifically requested backend is not available.
:exc:`!MissingBackendError` derives
from :exc:`RuntimeError`, since it usually indicates
lack of an external library or OS feature.
This is primarily raised by handlers which depend on
external libraries (which is currently just
:class:`~passlib.hash.bcrypt`).
"""
class PasswordSizeError(ValueError):
"""Error raised if a password exceeds the maximum size allowed
by Passlib (4096 characters).
Many password hash algorithms take proportionately larger amounts of time and/or
memory depending on the size of the password provided. This could present
a potential denial of service (DOS) situation if a maliciously large
password is provided to an application. Because of this, Passlib enforces
a maximum size limit, but one which should be *much* larger
than any legitimate password. :exc:`!PasswordSizeError` derives
from :exc:`!ValueError`.
.. note::
Applications wishing to use a different limit should set the
``PASSLIB_MAX_PASSWORD_SIZE`` environmental variable before
Passlib is loaded. The value can be any large positive integer.
.. versionadded:: 1.6
"""
def __init__(self):
ValueError.__init__(self, "password exceeds maximum allowed size")
# this also prevents a glibc crypt segfault issue, detailed here ...
# http://www.openwall.com/lists/oss-security/2011/11/15/1
#=============================================================================
# warnings
#=============================================================================
class PasslibWarning(UserWarning):
"""base class for Passlib's user warnings,
derives from the builtin :exc:`UserWarning`.
.. versionadded:: 1.6
"""
class PasslibConfigWarning(PasslibWarning):
"""Warning issued when non-fatal issue is found related to the configuration
of a :class:`~passlib.context.CryptContext` instance.
This occurs primarily in one of two cases:
* The CryptContext contains rounds limits which exceed the hard limits
imposed by the underlying algorithm.
* An explicit rounds value was provided which exceeds the limits
imposed by the CryptContext.
In both of these cases, the code will perform correctly & securely;
but the warning is issued as a sign the configuration may need updating.
.. versionadded:: 1.6
"""
class PasslibHashWarning(PasslibWarning):
"""Warning issued when non-fatal issue is found with parameters
or hash string passed to a passlib hash class.
This occurs primarily in one of two cases:
* A rounds value or other setting was explicitly provided which
exceeded the handler's limits (and has been clamped
by the :ref:`relaxed<relaxed-keyword>` flag).
* A malformed hash string was encountered which (while parsable)
should be re-encoded.
.. versionadded:: 1.6
"""
class PasslibRuntimeWarning(PasslibWarning):
"""Warning issued when something unexpected happens during runtime.
The fact that it's a warning instead of an error means Passlib
was able to correct for the issue, but that it's anonmalous enough
that the developers would love to hear under what conditions it occurred.
.. versionadded:: 1.6
"""
class PasslibSecurityWarning(PasslibWarning):
"""Special warning issued when Passlib encounters something
that might affect security.
.. versionadded:: 1.6
"""
#=============================================================================
# error constructors
#
# note: these functions are used by the hashes in Passlib to raise common
# error messages. They are currently just functions which return ValueError,
# rather than subclasses of ValueError, since the specificity isn't needed
# yet; and who wants to import a bunch of error classes when catching
# ValueError will do?
#=============================================================================
def _get_name(handler):
return handler.name if handler else "<unnamed>"
#------------------------------------------------------------------------
# generic helpers
#------------------------------------------------------------------------
def type_name(value):
"return pretty-printed string containing name of value's type"
cls = value.__class__
if cls.__module__ and cls.__module__ not in ["__builtin__", "builtins"]:
return "%s.%s" % (cls.__module__, cls.__name__)
elif value is None:
return 'None'
else:
return cls.__name__
def ExpectedTypeError(value, expected, param):
"error message when param was supposed to be one type, but found another"
# NOTE: value is never displayed, since it may sometimes be a password.
name = type_name(value)
return TypeError("%s must be %s, not %s" % (param, expected, name))
def ExpectedStringError(value, param):
"error message when param was supposed to be unicode or bytes"
return ExpectedTypeError(value, "unicode or bytes", param)
#------------------------------------------------------------------------
# encrypt/verify parameter errors
#------------------------------------------------------------------------
def MissingDigestError(handler=None):
"raised when verify() method gets passed config string instead of hash"
name = _get_name(handler)
return ValueError("expected %s hash, got %s config string instead" %
(name, name))
def NullPasswordError(handler=None):
"raised by OS crypt() supporting hashes, which forbid NULLs in password"
name = _get_name(handler)
return ValueError("%s does not allow NULL bytes in password" % name)
#------------------------------------------------------------------------
# errors when parsing hashes
#------------------------------------------------------------------------
def InvalidHashError(handler=None):
"error raised if unrecognized hash provided to handler"
return ValueError("not a valid %s hash" % _get_name(handler))
def MalformedHashError(handler=None, reason=None):
"error raised if recognized-but-malformed hash provided to handler"
text = "malformed %s hash" % _get_name(handler)
if reason:
text = "%s (%s)" % (text, reason)
return ValueError(text)
def ZeroPaddedRoundsError(handler=None):
"error raised if hash was recognized but contained zero-padded rounds field"
return MalformedHashError(handler, "zero-padded rounds")
#------------------------------------------------------------------------
# settings / hash component errors
#------------------------------------------------------------------------
def ChecksumSizeError(handler, raw=False):
"error raised if hash was recognized, but checksum was wrong size"
# TODO: if handler.use_defaults is set, this came from app-provided value,
# not from parsing a hash string, might want different error msg.
checksum_size = handler.checksum_size
unit = "bytes" if raw else "chars"
reason = "checksum must be exactly %d %s" % (checksum_size, unit)
return MalformedHashError(handler, reason)
#=============================================================================
# eof
#=============================================================================

1
passlib/ext/__init__.py Normal file
View File

@ -0,0 +1 @@

View File

@ -0,0 +1,6 @@
"""passlib.ext.django.models -- monkeypatch django hashing framework
this plugin monkeypatches django's hashing framework
so that it uses a passlib context object, allowing handling of arbitrary
hashes in Django databases.
"""

View File

@ -0,0 +1,323 @@
"""passlib.ext.django.models -- monkeypatch django hashing framework"""
#=============================================================================
# imports
#=============================================================================
# core
import logging; log = logging.getLogger(__name__)
from warnings import warn
# site
from django import VERSION
from django.conf import settings
# pkg
from passlib.context import CryptContext
from passlib.exc import ExpectedTypeError
from passlib.ext.django.utils import _PatchManager, hasher_to_passlib_name, \
get_passlib_hasher, get_preset_config
from passlib.utils.compat import callable, unicode, bytes
# local
__all__ = ["password_context"]
#=============================================================================
# global attrs
#=============================================================================
# the context object which this patches contrib.auth to use for password hashing.
# configuration controlled by ``settings.PASSLIB_CONFIG``.
password_context = CryptContext()
# function mapping User objects -> passlib user category.
# may be overridden via ``settings.PASSLIB_GET_CATEGORY``.
def _get_category(user):
"""default get_category() implementation"""
if user.is_superuser:
return "superuser"
elif user.is_staff:
return "staff"
else:
return None
# object used to track state of patches applied to django.
_manager = _PatchManager(log=logging.getLogger(__name__ + "._manager"))
# patch status
_patched = False
#=============================================================================
# applying & removing the patches
#=============================================================================
def _apply_patch():
"""monkeypatch django's password handling to use ``passlib_context``,
assumes the caller will configure the object.
"""
#
# setup constants
#
log.debug("preparing to monkeypatch 'django.contrib.auth' ...")
global _patched
assert not _patched, "monkeypatching already applied"
HASHERS_PATH = "django.contrib.auth.hashers"
MODELS_PATH = "django.contrib.auth.models"
USER_PATH = MODELS_PATH + ":User"
FORMS_PATH = "django.contrib.auth.forms"
#
# import UNUSUABLE_PASSWORD and is_password_usuable() helpers
# (providing stubs for older django versions)
#
if VERSION < (1,4):
has_hashers = False
if VERSION < (1,0):
UNUSABLE_PASSWORD = "!"
else:
from django.contrib.auth.models import UNUSABLE_PASSWORD
def is_password_usable(encoded):
return encoded is not None and encoded != UNUSABLE_PASSWORD
def is_valid_secret(secret):
return secret is not None
elif VERSION < (1,6):
has_hashers = True
from django.contrib.auth.hashers import UNUSABLE_PASSWORD, \
is_password_usable
# NOTE: 1.4 - 1.5 - empty passwords no longer valid.
def is_valid_secret(secret):
return bool(secret)
else:
has_hashers = True
from django.contrib.auth.hashers import is_password_usable
# 1.6 - empty passwords valid again
def is_valid_secret(secret):
return secret is not None
if VERSION < (1,6):
def make_unusable_password():
return UNUSABLE_PASSWORD
else:
from django.contrib.auth.hashers import make_password as _make_password
def make_unusable_password():
return _make_password(None)
# django 1.4.6+ uses a separate hasher for "sha1$$digest" hashes
has_unsalted_sha1 = (VERSION >= (1,4,6))
#
# backport ``User.set_unusable_password()`` for Django 0.9
# (simplifies rest of the code)
#
if not hasattr(_manager.getorig(USER_PATH), "set_unusable_password"):
assert VERSION < (1,0)
@_manager.monkeypatch(USER_PATH)
def set_unusable_password(user):
user.password = make_unusable_password()
@_manager.monkeypatch(USER_PATH)
def has_usable_password(user):
return is_password_usable(user.password)
#
# patch ``User.set_password() & ``User.check_password()`` to use
# context & get_category (would just leave these as wrappers for hashers
# module under django 1.4, but then we couldn't pass User object into
# get_category very easily)
#
@_manager.monkeypatch(USER_PATH)
def set_password(user, password):
"passlib replacement for User.set_password()"
if is_valid_secret(password):
# NOTE: pulls _get_category from module globals
cat = _get_category(user)
user.password = password_context.encrypt(password, category=cat)
else:
user.set_unusable_password()
@_manager.monkeypatch(USER_PATH)
def check_password(user, password):
"passlib replacement for User.check_password()"
hash = user.password
if not is_valid_secret(password) or not is_password_usable(hash):
return False
if not hash and VERSION < (1,4):
return False
# NOTE: pulls _get_category from module globals
cat = _get_category(user)
ok, new_hash = password_context.verify_and_update(password, hash,
category=cat)
if ok and new_hash is not None:
# migrate to new hash if needed.
user.password = new_hash
user.save()
return ok
#
# override check_password() with our own implementation
#
@_manager.monkeypatch(HASHERS_PATH, enable=has_hashers)
@_manager.monkeypatch(MODELS_PATH)
def check_password(password, encoded, setter=None, preferred="default"):
"passlib replacement for check_password()"
# XXX: this currently ignores "preferred" keyword, since it's purpose
# was for hash migration, and that's handled by the context.
if not is_valid_secret(password) or not is_password_usable(encoded):
return False
ok = password_context.verify(password, encoded)
if ok and setter and password_context.needs_update(encoded):
setter(password)
return ok
#
# patch the other functions defined in the ``hashers`` module, as well
# as any other known locations where they're imported within ``contrib.auth``
#
if has_hashers:
@_manager.monkeypatch(HASHERS_PATH)
@_manager.monkeypatch(MODELS_PATH)
def make_password(password, salt=None, hasher="default"):
"passlib replacement for make_password()"
if not is_valid_secret(password):
return make_unusable_password()
if hasher == "default":
scheme = None
else:
scheme = hasher_to_passlib_name(hasher)
kwds = dict(scheme=scheme)
handler = password_context.handler(scheme)
# NOTE: django make specify an empty string for the salt,
# even if scheme doesn't accept a salt. we omit keyword
# in that case.
if salt is not None and (salt or 'salt' in handler.setting_kwds):
kwds['salt'] = salt
return password_context.encrypt(password, **kwds)
@_manager.monkeypatch(HASHERS_PATH)
@_manager.monkeypatch(FORMS_PATH)
def get_hasher(algorithm="default"):
"passlib replacement for get_hasher()"
if algorithm == "default":
scheme = None
else:
scheme = hasher_to_passlib_name(algorithm)
# NOTE: resolving scheme -> handler instead of
# passing scheme into get_passlib_hasher(),
# in case context contains custom handler
# shadowing name of a builtin handler.
handler = password_context.handler(scheme)
return get_passlib_hasher(handler, algorithm=algorithm)
# identify_hasher() was added in django 1.5,
# patching it anyways for 1.4, so passlib's version is always available.
@_manager.monkeypatch(HASHERS_PATH)
@_manager.monkeypatch(FORMS_PATH)
def identify_hasher(encoded):
"passlib helper to identify hasher from encoded password"
handler = password_context.identify(encoded, resolve=True,
required=True)
algorithm = None
if (has_unsalted_sha1 and handler.name == "django_salted_sha1" and
encoded.startswith("sha1$$")):
# django 1.4.6+ uses a separate hasher for "sha1$$digest" hashes,
# but passlib just reuses the "sha1$salt$digest" handler.
# we want to resolve to correct django hasher.
algorithm = "unsalted_sha1"
return get_passlib_hasher(handler, algorithm=algorithm)
_patched = True
log.debug("... finished monkeypatching django")
def _remove_patch():
"""undo the django monkeypatching done by this module.
offered as a last resort if it's ever needed.
.. warning::
This may cause problems if any other Django modules have imported
their own copies of the patched functions, though the patched
code has been designed to throw an error as soon as possible in
this case.
"""
global _patched
if _patched:
log.debug("removing django monkeypatching...")
_manager.unpatch_all(unpatch_conflicts=True)
password_context.load({})
_patched = False
log.debug("...finished removing django monkeypatching")
return True
if _manager: # pragma: no cover -- sanity check
log.warning("reverting partial monkeypatching of django...")
_manager.unpatch_all()
password_context.load({})
log.debug("...finished removing django monkeypatching")
return True
log.debug("django not monkeypatched")
return False
#=============================================================================
# main code
#=============================================================================
def _load():
global _get_category
# TODO: would like to add support for inheriting config from a preset
# (or from existing hasher state) and letting PASSLIB_CONFIG
# be an update, not a replacement.
# TODO: wrap and import any custom hashers as passlib handlers,
# so they could be used in the passlib config.
# load config from settings
_UNSET = object()
config = getattr(settings, "PASSLIB_CONFIG", _UNSET)
if config is _UNSET:
# XXX: should probably deprecate this alias
config = getattr(settings, "PASSLIB_CONTEXT", _UNSET)
if config is _UNSET:
config = "passlib-default"
if config is None:
warn("setting PASSLIB_CONFIG=None is deprecated, "
"and support will be removed in Passlib 1.8, "
"use PASSLIB_CONFIG='disabled' instead.",
DeprecationWarning)
config = "disabled"
elif not isinstance(config, (unicode, bytes, dict)):
raise ExpectedTypeError(config, "str or dict", "PASSLIB_CONFIG")
# load custom category func (if any)
get_category = getattr(settings, "PASSLIB_GET_CATEGORY", None)
if get_category and not callable(get_category):
raise ExpectedTypeError(get_category, "callable", "PASSLIB_GET_CATEGORY")
# check if we've been disabled
if config == "disabled":
if _patched: # pragma: no cover -- sanity check
log.error("didn't expect monkeypatching would be applied!")
_remove_patch()
return
# resolve any preset aliases
if isinstance(config, str) and '\n' not in config:
config = get_preset_config(config)
# setup context
_apply_patch()
password_context.load(config)
if get_category:
# NOTE: _get_category is module global which is read by
# monkeypatched functions constructed by _apply_patch()
_get_category = get_category
log.debug("passlib.ext.django loaded")
# wrap load function so we can undo any patching if something goes wrong
try:
_load()
except:
_remove_patch()
raise
#=============================================================================
# eof
#=============================================================================

505
passlib/ext/django/utils.py Normal file
View File

@ -0,0 +1,505 @@
"""passlib.ext.django.utils - helper functions used by this plugin"""
#=============================================================================
# imports
#=============================================================================
# core
import logging; log = logging.getLogger(__name__)
from weakref import WeakKeyDictionary
from warnings import warn
# site
try:
from django import VERSION as DJANGO_VERSION
log.debug("found django %r installation", DJANGO_VERSION)
except ImportError:
log.debug("django installation not found")
DJANGO_VERSION = ()
# pkg
from passlib.context import CryptContext
from passlib.exc import PasslibRuntimeWarning
from passlib.registry import get_crypt_handler, list_crypt_handlers
from passlib.utils import classproperty
from passlib.utils.compat import bytes, get_method_function, iteritems
# local
__all__ = [
"get_preset_config",
"get_passlib_hasher",
]
#=============================================================================
# default policies
#=============================================================================
# map preset names -> passlib.app attrs
_preset_map = {
"django-1.0": "django10_context",
"django-1.4": "django14_context",
"django-1.6": "django16_context",
"django-latest": "django_context",
}
def get_preset_config(name):
"""Returns configuration string for one of the preset strings
supported by the ``PASSLIB_CONFIG`` setting.
Currently supported presets:
* ``"passlib-default"`` - default config used by this release of passlib.
* ``"django-default"`` - config matching currently installed django version.
* ``"django-latest"`` - config matching newest django version (currently same as ``"django-1.6"``).
* ``"django-1.0"`` - config used by stock Django 1.0 - 1.3 installs
* ``"django-1.4"`` - config used by stock Django 1.4 installs
* ``"django-1.6"`` - config used by stock Django 1.6 installs
"""
# TODO: add preset which includes HASHERS + PREFERRED_HASHERS,
# after having imported any custom hashers. e.g. "django-current"
if name == "django-default":
if not DJANGO_VERSION:
raise ValueError("can't resolve django-default preset, "
"django not installed")
if DJANGO_VERSION < (1,4):
name = "django-1.0"
elif DJANGO_VERSION < (1,6):
name = "django-1.4"
else:
name = "django-1.6"
if name == "passlib-default":
return PASSLIB_DEFAULT
try:
attr = _preset_map[name]
except KeyError:
raise ValueError("unknown preset config name: %r" % name)
import passlib.apps
return getattr(passlib.apps, attr).to_string()
# default context used by passlib 1.6
PASSLIB_DEFAULT = """
[passlib]
; list of schemes supported by configuration
; currently all django 1.6, 1.4, and 1.0 hashes,
; and three common modular crypt format hashes.
schemes =
django_pbkdf2_sha256, django_pbkdf2_sha1, django_bcrypt, django_bcrypt_sha256,
django_salted_sha1, django_salted_md5, django_des_crypt, hex_md5,
sha512_crypt, bcrypt, phpass
; default scheme to use for new hashes
default = django_pbkdf2_sha256
; hashes using these schemes will automatically be re-hashed
; when the user logs in (currently all django 1.0 hashes)
deprecated =
django_pbkdf2_sha1, django_salted_sha1, django_salted_md5,
django_des_crypt, hex_md5
; sets some common options, including minimum rounds for two primary hashes.
; if a hash has less than this number of rounds, it will be re-hashed.
all__vary_rounds = 0.05
sha512_crypt__min_rounds = 80000
django_pbkdf2_sha256__min_rounds = 10000
; set somewhat stronger iteration counts for ``User.is_staff``
staff__sha512_crypt__default_rounds = 100000
staff__django_pbkdf2_sha256__default_rounds = 12500
; and even stronger ones for ``User.is_superuser``
superuser__sha512_crypt__default_rounds = 120000
superuser__django_pbkdf2_sha256__default_rounds = 15000
"""
#=============================================================================
# translating passlib names <-> hasher names
#=============================================================================
# prefix used to shoehorn passlib's handler names into django hasher namespace;
# allows get_hasher() to be meaningfully called even if passlib handler
# is the one being used.
PASSLIB_HASHER_PREFIX = "passlib_"
# prefix all the django-specific hash formats are stored under w/in passlib;
# all of these hashes should expose their hasher name via ``.django_name``.
DJANGO_PASSLIB_PREFIX = "django_"
# non-django-specific hashes which also expose ``.django_name``.
_other_django_hashes = ["hex_md5"]
def passlib_to_hasher_name(passlib_name):
"convert passlib handler name -> hasher name"
handler = get_crypt_handler(passlib_name)
if hasattr(handler, "django_name"):
return handler.django_name
return PASSLIB_HASHER_PREFIX + passlib_name
def hasher_to_passlib_name(hasher_name):
"convert hasher name -> passlib handler name"
if hasher_name.startswith(PASSLIB_HASHER_PREFIX):
return hasher_name[len(PASSLIB_HASHER_PREFIX):]
if hasher_name == "unsalted_sha1":
# django 1.4.6+ uses a separate hasher for "sha1$$digest" hashes,
# but passlib just reuses the "sha1$salt$digest" handler.
hasher_name = "sha1"
for name in list_crypt_handlers():
if name.startswith(DJANGO_PASSLIB_PREFIX) or name in _other_django_hashes:
handler = get_crypt_handler(name)
if getattr(handler, "django_name", None) == hasher_name:
return name
# XXX: this should only happen for custom hashers that have been registered.
# _HasherHandler (below) is work in progress that would fix this.
raise ValueError("can't translate hasher name to passlib name: %r" %
hasher_name)
#=============================================================================
# wrapping passlib handlers as django hashers
#=============================================================================
_GEN_SALT_SIGNAL = "--!!!generate-new-salt!!!--"
class _HasherWrapper(object):
"""helper for wrapping passlib handlers in Hasher-compatible class."""
# filled in by subclass, drives the other methods.
passlib_handler = None
iterations = None
@classproperty
def algorithm(cls):
assert not hasattr(cls.passlib_handler, "django_name")
return PASSLIB_HASHER_PREFIX + cls.passlib_handler.name
def salt(self):
# NOTE: passlib's handler.encrypt() should generate new salt each time,
# so this just returns a special constant which tells
# encode() (below) not to pass a salt keyword along.
return _GEN_SALT_SIGNAL
def verify(self, password, encoded):
return self.passlib_handler.verify(password, encoded)
def encode(self, password, salt=None, iterations=None):
kwds = {}
if salt is not None and salt != _GEN_SALT_SIGNAL:
kwds['salt'] = salt
if iterations is not None:
kwds['rounds'] = iterations
elif self.iterations is not None:
kwds['rounds'] = self.iterations
return self.passlib_handler.encrypt(password, **kwds)
_translate_kwds = dict(checksum="hash", rounds="iterations")
def safe_summary(self, encoded):
from django.contrib.auth.hashers import mask_hash, _, SortedDict
handler = self.passlib_handler
items = [
# since this is user-facing, we're reporting passlib's name,
# without the distracting PASSLIB_HASHER_PREFIX prepended.
(_('algorithm'), handler.name),
]
if hasattr(handler, "parsehash"):
kwds = handler.parsehash(encoded, sanitize=mask_hash)
for key, value in iteritems(kwds):
key = self._translate_kwds.get(key, key)
items.append((_(key), value))
return SortedDict(items)
# added in django 1.6
def must_update(self, encoded):
# TODO: would like to do something useful here,
# but would require access to password context,
# which would mean a serious recoding of this ext.
return False
# cache of hasher wrappers generated by get_passlib_hasher()
_hasher_cache = WeakKeyDictionary()
def get_passlib_hasher(handler, algorithm=None):
"""create *Hasher*-compatible wrapper for specified passlib hash.
This takes in the name of a passlib hash (or the handler object itself),
and returns a wrapper instance which should be compatible with
Django 1.4's Hashers framework.
If the named hash corresponds to one of Django's builtin hashers,
an instance of the real hasher class will be returned.
Note that the format of the handler won't be altered,
so will probably not be compatible with Django's algorithm format,
so the monkeypatch provided by this plugin must have been applied.
.. note::
This function requires Django 1.4 or later.
"""
if DJANGO_VERSION < (1,4):
raise RuntimeError("get_passlib_hasher() requires Django >= 1.4")
if isinstance(handler, str):
handler = get_crypt_handler(handler)
if hasattr(handler, "django_name"):
# return native hasher instance
# XXX: should add this to _hasher_cache[]
name = handler.django_name
if name == "sha1" and algorithm == "unsalted_sha1":
# django 1.4.6+ uses a separate hasher for "sha1$$digest" hashes,
# but passlib just reuses the "sha1$salt$digest" handler.
# we want to resolve to correct django hasher.
name = algorithm
return _get_hasher(name)
if handler.name == "django_disabled":
raise ValueError("can't wrap unusable-password handler")
try:
return _hasher_cache[handler]
except KeyError:
name = "Passlib_%s_PasswordHasher" % handler.name.title()
cls = type(name, (_HasherWrapper,), dict(passlib_handler=handler))
hasher = _hasher_cache[handler] = cls()
return hasher
def _get_hasher(algorithm):
"wrapper to call django.contrib.auth.hashers:get_hasher()"
import sys
module = sys.modules.get("passlib.ext.django.models")
if module is None:
# we haven't patched django, so just import directly
from django.contrib.auth.hashers import get_hasher
else:
# we've patched django, so have to use patch manager to retreive
# original get_hasher() function...
get_hasher = module._manager.getorig("django.contrib.auth.hashers:get_hasher")
return get_hasher(algorithm)
#=============================================================================
# adapting django hashers -> passlib handlers
#=============================================================================
# TODO: this code probably halfway works, mainly just needs
# a routine to read HASHERS and PREFERRED_HASHER.
##from passlib.registry import register_crypt_handler
##from passlib.utils import classproperty, to_native_str, to_unicode
##from passlib.utils.compat import unicode
##
##
##class _HasherHandler(object):
## "helper for wrapping Hasher instances as passlib handlers"
## # FIXME: this generic wrapper doesn't handle custom settings
## # FIXME: genconfig / genhash not supported.
##
## def __init__(self, hasher):
## self.django_hasher = hasher
## if hasattr(hasher, "iterations"):
## # assume encode() accepts an "iterations" parameter.
## # fake min/max rounds
## self.min_rounds = 1
## self.max_rounds = 0xFFFFffff
## self.default_rounds = self.django_hasher.iterations
## self.setting_kwds += ("rounds",)
##
## # hasher instance - filled in by constructor
## django_hasher = None
##
## setting_kwds = ("salt",)
## context_kwds = ()
##
## @property
## def name(self):
## # XXX: need to make sure this wont' collide w/ builtin django hashes.
## # maybe by renaming this to django compatible aliases?
## return DJANGO_PASSLIB_PREFIX + self.django_name
##
## @property
## def django_name(self):
## # expose this so hasher_to_passlib_name() extracts original name
## return self.django_hasher.algorithm
##
## @property
## def ident(self):
## # this should always be correct, as django relies on ident prefix.
## return unicode(self.django_name + "$")
##
## @property
## def identify(self, hash):
## # this should always work, as django relies on ident prefix.
## return to_unicode(hash, "latin-1", "hash").startswith(self.ident)
##
## @property
## def genconfig(self):
## # XXX: not sure how to support this.
## return None
##
## @property
## def genhash(self, secret, config):
## if config is not None:
## # XXX: not sure how to support this.
## raise NotImplementedError("genhash() for hashers not implemented")
## return self.encrypt(secret)
##
## @property
## def encrypt(self, secret, salt=None, **kwds):
## # NOTE: from how make_password() is coded, all hashers
## # should have salt param. but only some will have
## # 'iterations' parameter.
## opts = {}
## if 'rounds' in self.setting_kwds and 'rounds' in kwds:
## opts['iterations'] = kwds.pop("rounds")
## if kwds:
## raise TypeError("unexpected keyword arguments: %r" % list(kwds))
## if isinstance(secret, unicode):
## secret = secret.encode("utf-8")
## if salt is None:
## salt = self.django_hasher.salt()
## return to_native_str(self.django_hasher(secret, salt, **opts))
##
## @property
## def verify(self, secret, hash):
## hash = to_native_str(hash, "utf-8", "hash")
## if isinstance(secret, unicode):
## secret = secret.encode("utf-8")
## return self.django_hasher.verify(secret, hash)
##
##def register_hasher(hasher):
## handler = _HasherHandler(hasher)
## register_crypt_handler(handler)
## return handler
#=============================================================================
# monkeypatch helpers
#=============================================================================
# private singleton indicating lack-of-value
_UNSET = object()
class _PatchManager(object):
"helper to manage monkeypatches and run sanity checks"
# NOTE: this could easily use a dict interface,
# but keeping it distinct to make clear that it's not a dict,
# since it has important side-effects.
#===================================================================
# init and support
#===================================================================
def __init__(self, log=None):
# map of key -> (original value, patched value)
# original value may be _UNSET
self.log = log or logging.getLogger(__name__ + "._PatchManager")
self._state = {}
# bool value tests if any patches are currently applied.
__bool__ = __nonzero__ = lambda self: bool(self._state)
def _import_path(self, path):
"retrieve obj and final attribute name from resource path"
name, attr = path.split(":")
obj = __import__(name, fromlist=[attr], level=0)
while '.' in attr:
head, attr = attr.split(".", 1)
obj = getattr(obj, head)
return obj, attr
@staticmethod
def _is_same_value(left, right):
"check if two values are the same (stripping method wrappers, etc)"
return get_method_function(left) == get_method_function(right)
#===================================================================
# reading
#===================================================================
def _get_path(self, key, default=_UNSET):
obj, attr = self._import_path(key)
return getattr(obj, attr, default)
def get(self, path, default=None):
"return current value for path"
return self._get_path(path, default)
def getorig(self, path, default=None):
"return original (unpatched) value for path"
try:
value, _= self._state[path]
except KeyError:
value = self._get_path(path)
return default if value is _UNSET else value
def check_all(self, strict=False):
"""run sanity check on all keys, issue warning if out of sync"""
same = self._is_same_value
for path, (orig, expected) in iteritems(self._state):
if same(self._get_path(path), expected):
continue
msg = "another library has patched resource: %r" % path
if strict:
raise RuntimeError(msg)
else:
warn(msg, PasslibRuntimeWarning)
#===================================================================
# patching
#===================================================================
def _set_path(self, path, value):
obj, attr = self._import_path(path)
if value is _UNSET:
if hasattr(obj, attr):
delattr(obj, attr)
else:
setattr(obj, attr, value)
def patch(self, path, value):
"monkeypatch object+attr at <path> to have <value>, stores original"
assert value != _UNSET
current = self._get_path(path)
try:
orig, expected = self._state[path]
except KeyError:
self.log.debug("patching resource: %r", path)
orig = current
else:
self.log.debug("modifying resource: %r", path)
if not self._is_same_value(current, expected):
warn("overridding resource another library has patched: %r"
% path, PasslibRuntimeWarning)
self._set_path(path, value)
self._state[path] = (orig, value)
##def patch_many(self, **kwds):
## "override specified resources with new values"
## for path, value in iteritems(kwds):
## self.patch(path, value)
def monkeypatch(self, parent, name=None, enable=True):
"function decorator which patches function of same name in <parent>"
def builder(func):
if enable:
sep = "." if ":" in parent else ":"
path = parent + sep + (name or func.__name__)
self.patch(path, func)
return func
return builder
#===================================================================
# unpatching
#===================================================================
def unpatch(self, path, unpatch_conflicts=True):
try:
orig, expected = self._state[path]
except KeyError:
return
current = self._get_path(path)
self.log.debug("unpatching resource: %r", path)
if not self._is_same_value(current, expected):
if unpatch_conflicts:
warn("reverting resource another library has patched: %r"
% path, PasslibRuntimeWarning)
else:
warn("not reverting resource another library has patched: %r"
% path, PasslibRuntimeWarning)
del self._state[path]
return
self._set_path(path, orig)
del self._state[path]
def unpatch_all(self, **kwds):
for key in list(self._state):
self.unpatch(key, **kwds)
#===================================================================
# eoc
#===================================================================
#=============================================================================
# eof
#=============================================================================

View File

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

457
passlib/handlers/bcrypt.py Normal file
View File

@ -0,0 +1,457 @@
"""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
#=============================================================================

219
passlib/handlers/cisco.py Normal file
View File

@ -0,0 +1,219 @@
"""passlib.handlers.cisco - Cisco password hashes"""
#=============================================================================
# imports
#=============================================================================
# core
from binascii import hexlify, unhexlify
from hashlib import md5
import logging; log = logging.getLogger(__name__)
from warnings import warn
# site
# pkg
from passlib.utils import h64, right_pad_string, to_unicode
from passlib.utils.compat import b, bascii_to_str, bytes, unicode, u, join_byte_values, \
join_byte_elems, byte_elem_value, iter_byte_values, uascii_to_str, str_to_uascii
import passlib.utils.handlers as uh
# local
__all__ = [
"cisco_pix",
"cisco_type7",
]
#=============================================================================
# cisco pix firewall hash
#=============================================================================
class cisco_pix(uh.HasUserContext, uh.StaticHandler):
"""This class implements the password hash used by Cisco PIX firewalls,
and follows the :ref:`password-hash-api`.
It does a single round of hashing, and relies on the username
as the salt.
The :meth:`~passlib.ifc.PasswordHash.encrypt`, :meth:`~passlib.ifc.PasswordHash.genhash`, and :meth:`~passlib.ifc.PasswordHash.verify` methods
have the following extra keyword:
:type user: str
:param user:
String containing name of user account this password is associated with.
This is *required* in order to correctly hash passwords associated
with a user account on the Cisco device, as it is used to salt
the hash.
Conversely, this *must* be omitted or set to ``""`` in order to correctly
hash passwords which don't have an associated user account
(such as the "enable" password).
"""
#===================================================================
# class attrs
#===================================================================
name = "cisco_pix"
checksum_size = 16
checksum_chars = uh.HASH64_CHARS
#===================================================================
# methods
#===================================================================
def _calc_checksum(self, secret):
if isinstance(secret, unicode):
# XXX: no idea what unicode policy is, but all examples are
# 7-bit ascii compatible, so using UTF-8
secret = secret.encode("utf-8")
user = self.user
if user:
# not positive about this, but it looks like per-user
# accounts use the first 4 chars of the username as the salt,
# whereas global "enable" passwords don't have any salt at all.
if isinstance(user, unicode):
user = user.encode("utf-8")
secret += user[:4]
# null-pad or truncate to 16 bytes
secret = right_pad_string(secret, 16)
# md5 digest
hash = md5(secret).digest()
# drop every 4th byte
hash = join_byte_elems(c for i,c in enumerate(hash) if i & 3 < 3)
# encode using Hash64
return h64.encode_bytes(hash).decode("ascii")
#===================================================================
# eoc
#===================================================================
#=============================================================================
# type 7
#=============================================================================
class cisco_type7(uh.GenericHandler):
"""This class implements the Type 7 password encoding used by Cisco IOS,
and follows the :ref:`password-hash-api`.
It has a simple 4-5 bit salt, but is nonetheless a reversible encoding
instead of a real hash.
The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genhash` methods
have the following optional keywords:
:type salt: int
:param salt:
This may be an optional salt integer drawn from ``range(0,16)``.
If omitted, one will be chosen at random.
:type relaxed: bool
:param relaxed:
By default, providing an invalid value for one of the other
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
will be issued instead. Correctable errors include
``salt`` values that are out of range.
Note that while this class outputs digests in upper-case hexidecimal,
it will accept lower-case as well.
This class also provides the following additional method:
.. automethod:: decode
"""
#===================================================================
# class attrs
#===================================================================
name = "cisco_type7"
setting_kwds = ("salt",)
checksum_chars = uh.UPPER_HEX_CHARS
# NOTE: encoding could handle max_salt_value=99, but since key is only 52
# chars in size, not sure what appropriate behavior is for that edge case.
min_salt_value = 0
max_salt_value = 52
#===================================================================
# methods
#===================================================================
@classmethod
def genconfig(cls):
return None
@classmethod
def genhash(cls, secret, config):
# special case to handle ``config=None`` in same style as StaticHandler
if config is None:
return cls.encrypt(secret)
else:
return super(cisco_type7, cls).genhash(secret, config)
@classmethod
def from_string(cls, hash):
hash = to_unicode(hash, "ascii", "hash")
if len(hash) < 2:
raise uh.exc.InvalidHashError(cls)
salt = int(hash[:2]) # may throw ValueError
return cls(salt=salt, checksum=hash[2:].upper())
def __init__(self, salt=None, **kwds):
super(cisco_type7, self).__init__(**kwds)
self.salt = self._norm_salt(salt)
def _norm_salt(self, salt):
"the salt for this algorithm is an integer 0-52, not a string"
# XXX: not entirely sure that values >15 are valid, so for
# compatibility we don't output those values, but we do accept them.
if salt is None:
if self.use_defaults:
salt = self._generate_salt()
else:
raise TypeError("no salt specified")
if not isinstance(salt, int):
raise uh.exc.ExpectedTypeError(salt, "integer", "salt")
if salt < 0 or salt > self.max_salt_value:
msg = "salt/offset must be in 0..52 range"
if self.relaxed:
warn(msg, uh.PasslibHashWarning)
salt = 0 if salt < 0 else self.max_salt_value
else:
raise ValueError(msg)
return salt
def _generate_salt(self):
return uh.rng.randint(0, 15)
def to_string(self):
return "%02d%s" % (self.salt, uascii_to_str(self.checksum))
def _calc_checksum(self, secret):
# XXX: no idea what unicode policy is, but all examples are
# 7-bit ascii compatible, so using UTF-8
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
return hexlify(self._cipher(secret, self.salt)).decode("ascii").upper()
@classmethod
def decode(cls, hash, encoding="utf-8"):
"""decode hash, returning original password.
:arg hash: encoded password
:param encoding: optional encoding to use (defaults to ``UTF-8``).
:returns: password as unicode
"""
self = cls.from_string(hash)
tmp = unhexlify(self.checksum.encode("ascii"))
raw = self._cipher(tmp, self.salt)
return raw.decode(encoding) if encoding else raw
# type7 uses a xor-based vingere variant, using the following secret key:
_key = u("dsfd;kfoA,.iyewrkldJKDHSUBsgvca69834ncxv9873254k;fg87")
@classmethod
def _cipher(cls, data, salt):
"xor static key against data - encrypts & decrypts"
key = cls._key
key_size = len(key)
return join_byte_values(
value ^ ord(key[(salt + idx) % key_size])
for idx, value in enumerate(iter_byte_values(data))
)
#=============================================================================
# eof
#=============================================================================

View File

@ -0,0 +1,517 @@
"""passlib.handlers.des_crypt - traditional unix (DES) crypt and variants"""
#=============================================================================
# imports
#=============================================================================
# core
import re
import logging; log = logging.getLogger(__name__)
from warnings import warn
# site
# pkg
from passlib.utils import classproperty, h64, h64big, safe_crypt, test_crypt, to_unicode
from passlib.utils.compat import b, bytes, byte_elem_value, u, uascii_to_str, unicode
from passlib.utils.des import des_encrypt_int_block
import passlib.utils.handlers as uh
# local
__all__ = [
"des_crypt",
"bsdi_crypt",
"bigcrypt",
"crypt16",
]
#=============================================================================
# pure-python backend for des_crypt family
#=============================================================================
_BNULL = b('\x00')
def _crypt_secret_to_key(secret):
"""convert secret to 64-bit DES key.
this only uses the first 8 bytes of the secret,
and discards the high 8th bit of each byte at that.
a null parity bit is inserted after every 7th bit of the output.
"""
# NOTE: this would set the parity bits correctly,
# but des_encrypt_int_block() would just ignore them...
##return sum(expand_7bit(byte_elem_value(c) & 0x7f) << (56-i*8)
## for i, c in enumerate(secret[:8]))
return sum((byte_elem_value(c) & 0x7f) << (57-i*8)
for i, c in enumerate(secret[:8]))
def _raw_des_crypt(secret, salt):
"pure-python backed for des_crypt"
assert len(salt) == 2
# NOTE: some OSes will accept non-HASH64 characters in the salt,
# but what value they assign these characters varies wildy,
# so just rejecting them outright.
# NOTE: the same goes for single-character salts...
# some OSes duplicate the char, some insert a '.' char,
# and openbsd does something which creates an invalid hash.
try:
salt_value = h64.decode_int12(salt)
except ValueError: # pragma: no cover - always caught by class
raise ValueError("invalid chars in salt")
# gotta do something - no official policy since this predates unicode
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
assert isinstance(secret, bytes)
# forbidding NULL char because underlying crypt() rejects them too.
if _BNULL in secret:
raise uh.exc.NullPasswordError(des_crypt)
# convert first 8 bytes of secret string into an integer
key_value = _crypt_secret_to_key(secret)
# run data through des using input of 0
result = des_encrypt_int_block(key_value, 0, salt_value, 25)
# run h64 encode on result
return h64big.encode_int64(result)
def _bsdi_secret_to_key(secret):
"covert secret to DES key used by bsdi_crypt"
key_value = _crypt_secret_to_key(secret)
idx = 8
end = len(secret)
while idx < end:
next = idx+8
tmp_value = _crypt_secret_to_key(secret[idx:next])
key_value = des_encrypt_int_block(key_value, key_value) ^ tmp_value
idx = next
return key_value
def _raw_bsdi_crypt(secret, rounds, salt):
"pure-python backend for bsdi_crypt"
# decode salt
try:
salt_value = h64.decode_int24(salt)
except ValueError: # pragma: no cover - always caught by class
raise ValueError("invalid salt")
# gotta do something - no official policy since this predates unicode
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
assert isinstance(secret, bytes)
# forbidding NULL char because underlying crypt() rejects them too.
if _BNULL in secret:
raise uh.exc.NullPasswordError(bsdi_crypt)
# convert secret string into an integer
key_value = _bsdi_secret_to_key(secret)
# run data through des using input of 0
result = des_encrypt_int_block(key_value, 0, salt_value, rounds)
# run h64 encode on result
return h64big.encode_int64(result)
#=============================================================================
# handlers
#=============================================================================
class des_crypt(uh.HasManyBackends, uh.HasSalt, uh.GenericHandler):
"""This class implements the des-crypt password hash, and follows the :ref:`password-hash-api`.
It supports a fixed-length salt.
The :meth:`~passlib.ifc.PasswordHash.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 2 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
:type relaxed: bool
:param relaxed:
By default, providing an invalid value for one of the other
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
will be issued instead. Correctable errors include
``salt`` strings that are too long.
.. versionadded:: 1.6
"""
#===================================================================
# class attrs
#===================================================================
#--GenericHandler--
name = "des_crypt"
setting_kwds = ("salt",)
checksum_chars = uh.HASH64_CHARS
checksum_size = 11
#--HasSalt--
min_salt_size = max_salt_size = 2
salt_chars = uh.HASH64_CHARS
#===================================================================
# formatting
#===================================================================
# FORMAT: 2 chars of H64-encoded salt + 11 chars of H64-encoded checksum
_hash_regex = re.compile(u(r"""
^
(?P<salt>[./a-z0-9]{2})
(?P<chk>[./a-z0-9]{11})?
$"""), re.X|re.I)
@classmethod
def from_string(cls, hash):
hash = to_unicode(hash, "ascii", "hash")
salt, chk = hash[:2], hash[2:]
return cls(salt=salt, checksum=chk or None)
def to_string(self):
hash = u("%s%s") % (self.salt, self.checksum or u(''))
return uascii_to_str(hash)
#===================================================================
# backend
#===================================================================
backends = ("os_crypt", "builtin")
_has_backend_builtin = True
@classproperty
def _has_backend_os_crypt(cls):
return test_crypt("test", 'abgOeLfPimXQo')
def _calc_checksum_builtin(self, secret):
return _raw_des_crypt(secret, self.salt.encode("ascii")).decode("ascii")
def _calc_checksum_os_crypt(self, secret):
# NOTE: safe_crypt encodes unicode secret -> utf8
# no official policy since des-crypt predates unicode
hash = safe_crypt(secret, self.salt)
if hash:
assert hash.startswith(self.salt) and len(hash) == 13
return hash[2:]
else:
return self._calc_checksum_builtin(secret)
#===================================================================
# eoc
#===================================================================
class bsdi_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler):
"""This class implements the BSDi-Crypt password hash, and follows the :ref:`password-hash-api`.
It supports a fixed-length salt, and a variable number of rounds.
The :meth:`~passlib.ifc.PasswordHash.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 4 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
:type rounds: int
:param rounds:
Optional number of rounds to use.
Defaults to 5001, must be between 1 and 16777215, inclusive.
:type relaxed: bool
:param relaxed:
By default, providing an invalid value for one of the other
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
will be issued instead. Correctable errors include ``rounds``
that are too small or too large, and ``salt`` strings that are too long.
.. versionadded:: 1.6
.. versionchanged:: 1.6
:meth:`encrypt` will now issue a warning if an even number of rounds is used
(see :ref:`bsdi-crypt-security-issues` regarding weak DES keys).
"""
#===================================================================
# class attrs
#===================================================================
#--GenericHandler--
name = "bsdi_crypt"
setting_kwds = ("salt", "rounds")
checksum_size = 11
checksum_chars = uh.HASH64_CHARS
#--HasSalt--
min_salt_size = max_salt_size = 4
salt_chars = uh.HASH64_CHARS
#--HasRounds--
default_rounds = 5001
min_rounds = 1
max_rounds = 16777215 # (1<<24)-1
rounds_cost = "linear"
# NOTE: OpenBSD login.conf reports 7250 as minimum allowed rounds,
# but that seems to be an OS policy, not a algorithm limitation.
#===================================================================
# parsing
#===================================================================
_hash_regex = re.compile(u(r"""
^
_
(?P<rounds>[./a-z0-9]{4})
(?P<salt>[./a-z0-9]{4})
(?P<chk>[./a-z0-9]{11})?
$"""), re.X|re.I)
@classmethod
def from_string(cls, hash):
hash = to_unicode(hash, "ascii", "hash")
m = cls._hash_regex.match(hash)
if not m:
raise uh.exc.InvalidHashError(cls)
rounds, salt, chk = m.group("rounds", "salt", "chk")
return cls(
rounds=h64.decode_int24(rounds.encode("ascii")),
salt=salt,
checksum=chk,
)
def to_string(self):
hash = u("_%s%s%s") % (h64.encode_int24(self.rounds).decode("ascii"),
self.salt, self.checksum or u(''))
return uascii_to_str(hash)
#===================================================================
# validation
#===================================================================
# flag so CryptContext won't generate even rounds.
_avoid_even_rounds = True
def _norm_rounds(self, rounds):
rounds = super(bsdi_crypt, self)._norm_rounds(rounds)
# issue warning if app provided an even rounds value
if self.use_defaults and not rounds & 1:
warn("bsdi_crypt rounds should be odd, "
"as even rounds may reveal weak DES keys",
uh.exc.PasslibSecurityWarning)
return rounds
@classmethod
def _bind_needs_update(cls, **settings):
return cls._needs_update
@classmethod
def _needs_update(cls, hash, secret):
# mark bsdi_crypt hashes as deprecated if they have even rounds.
assert cls.identify(hash)
if isinstance(hash, unicode):
hash = hash.encode("ascii")
rounds = h64.decode_int24(hash[1:5])
return not rounds & 1
#===================================================================
# backends
#===================================================================
backends = ("os_crypt", "builtin")
_has_backend_builtin = True
@classproperty
def _has_backend_os_crypt(cls):
return test_crypt("test", '_/...lLDAxARksGCHin.')
def _calc_checksum_builtin(self, secret):
return _raw_bsdi_crypt(secret, self.rounds, self.salt.encode("ascii")).decode("ascii")
def _calc_checksum_os_crypt(self, secret):
config = self.to_string()
hash = safe_crypt(secret, config)
if hash:
assert hash.startswith(config[:9]) and len(hash) == 20
return hash[-11:]
else:
return self._calc_checksum_builtin(secret)
#===================================================================
# eoc
#===================================================================
class bigcrypt(uh.HasSalt, uh.GenericHandler):
"""This class implements the BigCrypt password hash, and follows the :ref:`password-hash-api`.
It supports a fixed-length salt.
The :meth:`~passlib.ifc.PasswordHash.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 relaxed: bool
:param relaxed:
By default, providing an invalid value for one of the other
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
will be issued instead. Correctable errors include
``salt`` strings that are too long.
.. versionadded:: 1.6
"""
#===================================================================
# class attrs
#===================================================================
#--GenericHandler--
name = "bigcrypt"
setting_kwds = ("salt",)
checksum_chars = uh.HASH64_CHARS
# NOTE: checksum chars must be multiple of 11
#--HasSalt--
min_salt_size = max_salt_size = 2
salt_chars = uh.HASH64_CHARS
#===================================================================
# internal helpers
#===================================================================
_hash_regex = re.compile(u(r"""
^
(?P<salt>[./a-z0-9]{2})
(?P<chk>([./a-z0-9]{11})+)?
$"""), re.X|re.I)
@classmethod
def from_string(cls, hash):
hash = to_unicode(hash, "ascii", "hash")
m = cls._hash_regex.match(hash)
if not m:
raise uh.exc.InvalidHashError(cls)
salt, chk = m.group("salt", "chk")
return cls(salt=salt, checksum=chk)
def to_string(self):
hash = u("%s%s") % (self.salt, self.checksum or u(''))
return uascii_to_str(hash)
def _norm_checksum(self, value):
value = super(bigcrypt, self)._norm_checksum(value)
if value and len(value) % 11:
raise uh.exc.InvalidHashError(self)
return value
#===================================================================
# backend
#===================================================================
def _calc_checksum(self, secret):
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
chk = _raw_des_crypt(secret, self.salt.encode("ascii"))
idx = 8
end = len(secret)
while idx < end:
next = idx + 8
chk += _raw_des_crypt(secret[idx:next], chk[-11:-9])
idx = next
return chk.decode("ascii")
#===================================================================
# eoc
#===================================================================
class crypt16(uh.HasSalt, uh.GenericHandler):
"""This class implements the crypt16 password hash, and follows the :ref:`password-hash-api`.
It supports a fixed-length salt.
The :meth:`~passlib.ifc.PasswordHash.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 2 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
:type relaxed: bool
:param relaxed:
By default, providing an invalid value for one of the other
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
will be issued instead. Correctable errors include
``salt`` strings that are too long.
.. versionadded:: 1.6
"""
#===================================================================
# class attrs
#===================================================================
#--GenericHandler--
name = "crypt16"
setting_kwds = ("salt",)
checksum_size = 22
checksum_chars = uh.HASH64_CHARS
#--HasSalt--
min_salt_size = max_salt_size = 2
salt_chars = uh.HASH64_CHARS
#===================================================================
# internal helpers
#===================================================================
_hash_regex = re.compile(u(r"""
^
(?P<salt>[./a-z0-9]{2})
(?P<chk>[./a-z0-9]{22})?
$"""), re.X|re.I)
@classmethod
def from_string(cls, hash):
hash = to_unicode(hash, "ascii", "hash")
m = cls._hash_regex.match(hash)
if not m:
raise uh.exc.InvalidHashError(cls)
salt, chk = m.group("salt", "chk")
return cls(salt=salt, checksum=chk)
def to_string(self):
hash = u("%s%s") % (self.salt, self.checksum or u(''))
return uascii_to_str(hash)
#===================================================================
# backend
#===================================================================
def _calc_checksum(self, secret):
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
# parse salt value
try:
salt_value = h64.decode_int12(self.salt.encode("ascii"))
except ValueError: # pragma: no cover - caught by class
raise ValueError("invalid chars in salt")
# convert first 8 byts of secret string into an integer,
key1 = _crypt_secret_to_key(secret)
# run data through des using input of 0
result1 = des_encrypt_int_block(key1, 0, salt_value, 20)
# convert next 8 bytes of secret string into integer (key=0 if secret < 8 chars)
key2 = _crypt_secret_to_key(secret[8:16])
# run data through des using input of 0
result2 = des_encrypt_int_block(key2, 0, salt_value, 5)
# done
chk = h64big.encode_int64(result1) + h64big.encode_int64(result2)
return chk.decode("ascii")
#===================================================================
# eoc
#===================================================================
#=============================================================================
# eof
#=============================================================================

144
passlib/handlers/digests.py Normal file
View File

@ -0,0 +1,144 @@
"""passlib.handlers.digests - plain hash digests
"""
#=============================================================================
# imports
#=============================================================================
# core
import hashlib
import logging; log = logging.getLogger(__name__)
from warnings import warn
# site
# pkg
from passlib.utils import to_native_str, to_bytes, render_bytes, consteq
from passlib.utils.compat import bascii_to_str, bytes, unicode, str_to_uascii
import passlib.utils.handlers as uh
from passlib.utils.md4 import md4
# local
__all__ = [
"create_hex_hash",
"hex_md4",
"hex_md5",
"hex_sha1",
"hex_sha256",
"hex_sha512",
]
#=============================================================================
# helpers for hexidecimal hashes
#=============================================================================
class HexDigestHash(uh.StaticHandler):
"this provides a template for supporting passwords stored as plain hexidecimal hashes"
#===================================================================
# class attrs
#===================================================================
_hash_func = None # hash function to use - filled in by create_hex_hash()
checksum_size = None # filled in by create_hex_hash()
checksum_chars = uh.HEX_CHARS
#===================================================================
# methods
#===================================================================
@classmethod
def _norm_hash(cls, hash):
return hash.lower()
def _calc_checksum(self, secret):
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
return str_to_uascii(self._hash_func(secret).hexdigest())
#===================================================================
# eoc
#===================================================================
def create_hex_hash(hash, digest_name, module=__name__):
# NOTE: could set digest_name=hash.name for cpython, but not for some other platforms.
h = hash()
name = "hex_" + digest_name
return type(name, (HexDigestHash,), dict(
name=name,
__module__=module, # so ABCMeta won't clobber it
_hash_func=staticmethod(hash), # sometimes it's a function, sometimes not. so wrap it.
checksum_size=h.digest_size*2,
__doc__="""This class implements a plain hexidecimal %s hash, and follows the :ref:`password-hash-api`.
It supports no optional or contextual keywords.
""" % (digest_name,)
))
#=============================================================================
# predefined handlers
#=============================================================================
hex_md4 = create_hex_hash(md4, "md4")
hex_md5 = create_hex_hash(hashlib.md5, "md5")
hex_md5.django_name = "unsalted_md5"
hex_sha1 = create_hex_hash(hashlib.sha1, "sha1")
hex_sha256 = create_hex_hash(hashlib.sha256, "sha256")
hex_sha512 = create_hex_hash(hashlib.sha512, "sha512")
#=============================================================================
# htdigest
#=============================================================================
class htdigest(uh.PasswordHash):
"""htdigest hash function.
.. todo::
document this hash
"""
name = "htdigest"
setting_kwds = ()
context_kwds = ("user", "realm", "encoding")
default_encoding = "utf-8"
@classmethod
def encrypt(cls, secret, user, realm, encoding=None):
# NOTE: this was deliberately written so that raw bytes are passed through
# unchanged, the encoding kwd is only used to handle unicode values.
if not encoding:
encoding = cls.default_encoding
uh.validate_secret(secret)
if isinstance(secret, unicode):
secret = secret.encode(encoding)
user = to_bytes(user, encoding, "user")
realm = to_bytes(realm, encoding, "realm")
data = render_bytes("%s:%s:%s", user, realm, secret)
return hashlib.md5(data).hexdigest()
@classmethod
def _norm_hash(cls, hash):
"normalize hash to native string, and validate it"
hash = to_native_str(hash, param="hash")
if len(hash) != 32:
raise uh.exc.MalformedHashError(cls, "wrong size")
for char in hash:
if char not in uh.LC_HEX_CHARS:
raise uh.exc.MalformedHashError(cls, "invalid chars in hash")
return hash
@classmethod
def verify(cls, secret, hash, user, realm, encoding="utf-8"):
hash = cls._norm_hash(hash)
other = cls.encrypt(secret, user, realm, encoding)
return consteq(hash, other)
@classmethod
def identify(cls, hash):
try:
cls._norm_hash(hash)
except ValueError:
return False
return True
@classmethod
def genconfig(cls):
return None
@classmethod
def genhash(cls, secret, config, user, realm, encoding="utf-8"):
if config is not None:
cls._norm_hash(config)
return cls.encrypt(secret, user, realm, encoding)
#=============================================================================
# eof
#=============================================================================

472
passlib/handlers/django.py Normal file
View File

@ -0,0 +1,472 @@
"""passlib.handlers.django- Django password hash support"""
#=============================================================================
# imports
#=============================================================================
# core
from base64 import b64encode
from binascii import hexlify
from hashlib import md5, sha1, sha256
import re
import logging; log = logging.getLogger(__name__)
from warnings import warn
# site
# pkg
from passlib.hash import bcrypt, pbkdf2_sha1, pbkdf2_sha256
from passlib.utils import to_unicode, classproperty
from passlib.utils.compat import b, bytes, str_to_uascii, uascii_to_str, unicode, u
from passlib.utils.pbkdf2 import pbkdf2
import passlib.utils.handlers as uh
# local
__all__ = [
"django_salted_sha1",
"django_salted_md5",
"django_bcrypt",
"django_pbkdf2_sha1",
"django_pbkdf2_sha256",
"django_des_crypt",
"django_disabled",
]
#=============================================================================
# lazy imports & constants
#=============================================================================
# imported by django_des_crypt._calc_checksum()
des_crypt = None
def _import_des_crypt():
global des_crypt
if des_crypt is None:
from passlib.hash import des_crypt
return des_crypt
# django 1.4's salt charset
SALT_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
#=============================================================================
# salted hashes
#=============================================================================
class DjangoSaltedHash(uh.HasSalt, uh.GenericHandler):
"""base class providing common code for django hashes"""
# name, ident, checksum_size must be set by subclass.
# ident must include "$" suffix.
setting_kwds = ("salt", "salt_size")
min_salt_size = 0
# NOTE: django 1.0-1.3 would accept empty salt strings.
# django 1.4 won't, but this appears to be regression
# (https://code.djangoproject.com/ticket/18144)
# so presumably it will be fixed in a later release.
default_salt_size = 12
max_salt_size = None
salt_chars = SALT_CHARS
checksum_chars = uh.LOWER_HEX_CHARS
@classproperty
def _stub_checksum(cls):
return cls.checksum_chars[0] * cls.checksum_size
@classmethod
def from_string(cls, hash):
salt, chk = uh.parse_mc2(hash, cls.ident, handler=cls)
return cls(salt=salt, checksum=chk)
def to_string(self):
return uh.render_mc2(self.ident, self.salt,
self.checksum or self._stub_checksum)
class DjangoVariableHash(uh.HasRounds, DjangoSaltedHash):
"""base class providing common code for django hashes w/ variable rounds"""
setting_kwds = DjangoSaltedHash.setting_kwds + ("rounds",)
min_rounds = 1
@classmethod
def from_string(cls, hash):
rounds, salt, chk = uh.parse_mc3(hash, cls.ident, handler=cls)
return cls(rounds=rounds, salt=salt, checksum=chk)
def to_string(self):
return uh.render_mc3(self.ident, self.rounds, self.salt,
self.checksum or self._stub_checksum)
class django_salted_sha1(DjangoSaltedHash):
"""This class implements Django's Salted SHA1 hash, and follows the :ref:`password-hash-api`.
It supports a variable-length salt, and uses a single round of SHA1.
The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
:type salt: str
:param salt:
Optional salt string.
If not specified, a 12 character one will be autogenerated (this is recommended).
If specified, may be any series of characters drawn from the regexp range ``[0-9a-zA-Z]``.
:type salt_size: int
:param salt_size:
Optional number of characters to use when autogenerating new salts.
Defaults to 12, but can be any positive value.
This should be compatible with Django 1.4's :class:`!SHA1PasswordHasher` class.
.. versionchanged: 1.6
This class now generates 12-character salts instead of 5,
and generated salts uses the character range ``[0-9a-zA-Z]`` instead of
the ``[0-9a-f]``. This is to be compatible with how Django >= 1.4
generates these hashes; but hashes generated in this manner will still be
correctly interpreted by earlier versions of Django.
"""
name = "django_salted_sha1"
django_name = "sha1"
ident = u("sha1$")
checksum_size = 40
def _calc_checksum(self, secret):
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
return str_to_uascii(sha1(self.salt.encode("ascii") + secret).hexdigest())
class django_salted_md5(DjangoSaltedHash):
"""This class implements Django's Salted MD5 hash, and follows the :ref:`password-hash-api`.
It supports a variable-length salt, and uses a single round of MD5.
The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
:type salt: str
:param salt:
Optional salt string.
If not specified, a 12 character one will be autogenerated (this is recommended).
If specified, may be any series of characters drawn from the regexp range ``[0-9a-zA-Z]``.
:type salt_size: int
:param salt_size:
Optional number of characters to use when autogenerating new salts.
Defaults to 12, but can be any positive value.
This should be compatible with the hashes generated by
Django 1.4's :class:`!MD5PasswordHasher` class.
.. versionchanged: 1.6
This class now generates 12-character salts instead of 5,
and generated salts uses the character range ``[0-9a-zA-Z]`` instead of
the ``[0-9a-f]``. This is to be compatible with how Django >= 1.4
generates these hashes; but hashes generated in this manner will still be
correctly interpreted by earlier versions of Django.
"""
name = "django_salted_md5"
django_name = "md5"
ident = u("md5$")
checksum_size = 32
def _calc_checksum(self, secret):
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
return str_to_uascii(md5(self.salt.encode("ascii") + secret).hexdigest())
django_bcrypt = uh.PrefixWrapper("django_bcrypt", bcrypt,
prefix=u('bcrypt$'), ident=u("bcrypt$"),
# NOTE: this docstring is duplicated in the docs, since sphinx
# seems to be having trouble reading it via autodata::
doc="""This class implements Django 1.4's BCrypt wrapper, and follows the :ref:`password-hash-api`.
This is identical to :class:`!bcrypt` itself, but with
the Django-specific prefix ``"bcrypt$"`` prepended.
See :doc:`/lib/passlib.hash.bcrypt` for more details,
the usage and behavior is identical.
This should be compatible with the hashes generated by
Django 1.4's :class:`!BCryptPasswordHasher` class.
.. versionadded:: 1.6
""")
django_bcrypt.django_name = "bcrypt"
class django_bcrypt_sha256(bcrypt):
"""This class implements Django 1.6's Bcrypt+SHA256 hash, and follows the :ref:`password-hash-api`.
It supports a variable-length salt, and a variable number of rounds.
While the algorithm and format is somewhat different,
the api and options for this hash are identical to :class:`!bcrypt` itself,
see :doc:`/lib/passlib.hash.bcrypt` for more details.
.. versionadded:: 1.6.2
"""
name = "django_bcrypt_sha256"
django_name = "bcrypt_sha256"
_digest = sha256
# NOTE: django bcrypt ident locked at "$2a$", so omitting 'ident' support.
setting_kwds = ("salt", "rounds")
# sample hash:
# bcrypt_sha256$$2a$06$/3OeRpbOf8/l6nPPRdZPp.nRiyYqPobEZGdNRBWihQhiFDh1ws1tu
# XXX: we can't use .ident attr due to bcrypt code using it.
# working around that via django_prefix
django_prefix = u('bcrypt_sha256$')
@classmethod
def identify(cls, hash):
hash = uh.to_unicode_for_identify(hash)
if not hash:
return False
return hash.startswith(cls.django_prefix)
@classmethod
def from_string(cls, hash):
hash = to_unicode(hash, "ascii", "hash")
if not hash.startswith(cls.django_prefix):
raise uh.exc.InvalidHashError(cls)
bhash = hash[len(cls.django_prefix):]
if not bhash.startswith("$2"):
raise uh.exc.MalformedHashError(cls)
return super(django_bcrypt_sha256, cls).from_string(bhash)
def __init__(self, **kwds):
if 'ident' in kwds and kwds.get("use_defaults"):
raise TypeError("%s does not support the ident keyword" %
self.__class__.__name__)
return super(django_bcrypt_sha256, self).__init__(**kwds)
def to_string(self):
bhash = super(django_bcrypt_sha256, self).to_string()
return uascii_to_str(self.django_prefix) + bhash
def _calc_checksum(self, secret):
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
secret = hexlify(self._digest(secret).digest())
return super(django_bcrypt_sha256, self)._calc_checksum(secret)
# 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)
class django_pbkdf2_sha256(DjangoVariableHash):
"""This class implements Django's PBKDF2-HMAC-SHA256 hash, and follows the :ref:`password-hash-api`.
It supports a variable-length salt, and a variable number of rounds.
The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
:type salt: str
:param salt:
Optional salt string.
If not specified, a 12 character one will be autogenerated (this is recommended).
If specified, may be any series of characters drawn from the regexp range ``[0-9a-zA-Z]``.
:type salt_size: int
:param salt_size:
Optional number of characters to use when autogenerating new salts.
Defaults to 12, but can be any positive value.
:type rounds: int
:param rounds:
Optional number of rounds to use.
Defaults to 20000, but must be within ``range(1,1<<32)``.
:type relaxed: bool
:param relaxed:
By default, providing an invalid value for one of the other
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
will be issued instead. Correctable errors include ``rounds``
that are too small or too large, and ``salt`` strings that are too long.
This should be compatible with the hashes generated by
Django 1.4's :class:`!PBKDF2PasswordHasher` class.
.. versionadded:: 1.6
"""
name = "django_pbkdf2_sha256"
django_name = "pbkdf2_sha256"
ident = u('pbkdf2_sha256$')
min_salt_size = 1
max_rounds = 0xffffffff # setting at 32-bit limit for now
checksum_chars = uh.PADDED_BASE64_CHARS
checksum_size = 44 # 32 bytes -> base64
default_rounds = pbkdf2_sha256.default_rounds # NOTE: django 1.6 uses 12000
_prf = "hmac-sha256"
def _calc_checksum(self, secret):
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
hash = pbkdf2(secret, self.salt.encode("ascii"), self.rounds,
keylen=None, prf=self._prf)
return b64encode(hash).rstrip().decode("ascii")
class django_pbkdf2_sha1(django_pbkdf2_sha256):
"""This class implements Django's PBKDF2-HMAC-SHA1 hash, and follows the :ref:`password-hash-api`.
It supports a variable-length salt, and a variable number of rounds.
The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
:type salt: str
:param salt:
Optional salt string.
If not specified, a 12 character one will be autogenerated (this is recommended).
If specified, may be any series of characters drawn from the regexp range ``[0-9a-zA-Z]``.
:type salt_size: int
:param salt_size:
Optional number of characters to use when autogenerating new salts.
Defaults to 12, but can be any positive value.
:type rounds: int
:param rounds:
Optional number of rounds to use.
Defaults to 60000, but must be within ``range(1,1<<32)``.
:type relaxed: bool
:param relaxed:
By default, providing an invalid value for one of the other
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
will be issued instead. Correctable errors include ``rounds``
that are too small or too large, and ``salt`` strings that are too long.
This should be compatible with the hashes generated by
Django 1.4's :class:`!PBKDF2SHA1PasswordHasher` class.
.. versionadded:: 1.6
"""
name = "django_pbkdf2_sha1"
django_name = "pbkdf2_sha1"
ident = u('pbkdf2_sha1$')
checksum_size = 28 # 20 bytes -> base64
default_rounds = pbkdf2_sha1.default_rounds # NOTE: django 1.6 uses 12000
_prf = "hmac-sha1"
#=============================================================================
# other
#=============================================================================
class django_des_crypt(uh.HasSalt, uh.GenericHandler):
"""This class implements Django's :class:`des_crypt` wrapper, and follows the :ref:`password-hash-api`.
It supports a fixed-length salt.
The :meth:`~passlib.ifc.PasswordHash.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 2 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
This should be compatible with the hashes generated by
Django 1.4's :class:`!CryptPasswordHasher` class.
Note that Django only supports this hash on Unix systems
(though :class:`!django_des_crypt` is available cross-platform
under Passlib).
.. versionchanged:: 1.6
This class will now accept hashes with empty salt strings,
since Django 1.4 generates them this way.
"""
name = "django_des_crypt"
django_name = "crypt"
setting_kwds = ("salt", "salt_size")
ident = u("crypt$")
checksum_chars = salt_chars = uh.HASH64_CHARS
checksum_size = 11
min_salt_size = default_salt_size = 2
_stub_checksum = u('.')*11
# NOTE: regarding duplicate salt field:
#
# django 1.0 had a "crypt$<salt1>$<salt2><digest>" hash format,
# used [a-z0-9] to generate a 5 char salt, stored it in salt1,
# duplicated the first two chars of salt1 as salt2.
# it would throw an error if salt1 was empty.
#
# django 1.4 started generating 2 char salt using the full alphabet,
# left salt1 empty, and only paid attention to salt2.
#
# in order to be compatible with django 1.0, the hashes generated
# by this function will always include salt1, unless the following
# class-level field is disabled (mainly used for testing)
use_duplicate_salt = True
@classmethod
def from_string(cls, hash):
salt, chk = uh.parse_mc2(hash, cls.ident, handler=cls)
if chk:
# chk should be full des_crypt hash
if not salt:
# django 1.4 always uses empty salt field,
# so extract salt from des_crypt hash <chk>
salt = chk[:2]
elif salt[:2] != chk[:2]:
# django 1.0 stored 5 chars in salt field, and duplicated
# the first two chars in <chk>. we keep the full salt,
# but make sure the first two chars match as sanity check.
raise uh.exc.MalformedHashError(cls,
"first two digits of salt and checksum must match")
# in all cases, strip salt chars from <chk>
chk = chk[2:]
return cls(salt=salt, checksum=chk)
def to_string(self):
salt = self.salt
chk = salt[:2] + (self.checksum or self._stub_checksum)
if self.use_duplicate_salt:
# filling in salt field, so that we're compatible with django 1.0
return uh.render_mc2(self.ident, salt, chk)
else:
# django 1.4+ style hash
return uh.render_mc2(self.ident, "", chk)
def _calc_checksum(self, secret):
# NOTE: we lazily import des_crypt,
# since most django deploys won't use django_des_crypt
global des_crypt
if des_crypt is None:
_import_des_crypt()
return des_crypt(salt=self.salt[:2])._calc_checksum(secret)
class django_disabled(uh.StaticHandler):
"""This class provides disabled password behavior for Django, and follows the :ref:`password-hash-api`.
This class does not implement a hash, but instead
claims the special hash string ``"!"`` which Django uses
to indicate an account's password has been disabled.
* newly encrypted passwords will hash to ``"!"``.
* it rejects all passwords.
.. note::
Django 1.6 prepends a randomly generate 40-char alphanumeric string
to each unusuable password. This class recognizes such strings,
but for backwards compatibility, still returns ``"!"``.
.. versionchanged:: 1.6.2 added Django 1.6 support
"""
name = "django_disabled"
@classmethod
def identify(cls, hash):
hash = uh.to_unicode_for_identify(hash)
return hash.startswith(u("!"))
def _calc_checksum(self, secret):
return u("!")
@classmethod
def verify(cls, secret, hash):
uh.validate_secret(secret)
if not cls.identify(hash):
raise uh.exc.InvalidHashError(cls)
return False
#=============================================================================
# eof
#=============================================================================

206
passlib/handlers/fshp.py Normal file
View File

@ -0,0 +1,206 @@
"""passlib.handlers.fshp
"""
#=============================================================================
# imports
#=============================================================================
# core
from base64 import b64encode, b64decode
import re
import logging; log = logging.getLogger(__name__)
from warnings import warn
# site
# pkg
from passlib.utils import to_unicode
import passlib.utils.handlers as uh
from passlib.utils.compat import b, bytes, bascii_to_str, iteritems, u,\
unicode
from passlib.utils.pbkdf2 import pbkdf1
# local
__all__ = [
'fshp',
]
#=============================================================================
# sha1-crypt
#=============================================================================
class fshp(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
"""This class implements the FSHP password hash, and follows the :ref:`password-hash-api`.
It supports a variable-length salt, and a variable number of rounds.
The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
:param salt:
Optional raw salt string.
If not specified, one will be autogenerated (this is recommended).
:param salt_size:
Optional number of bytes to use when autogenerating new salts.
Defaults to 16 bytes, but can be any non-negative value.
:param rounds:
Optional number of rounds to use.
Defaults to 100000, must be between 1 and 4294967295, inclusive.
:param variant:
Optionally specifies variant of FSHP to use.
* ``0`` - uses SHA-1 digest (deprecated).
* ``1`` - uses SHA-2/256 digest (default).
* ``2`` - uses SHA-2/384 digest.
* ``3`` - uses SHA-2/512 digest.
:type relaxed: bool
:param relaxed:
By default, providing an invalid value for one of the other
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
will be issued instead. Correctable errors include ``rounds``
that are too small or too large, and ``salt`` strings that are too long.
.. versionadded:: 1.6
"""
#===================================================================
# class attrs
#===================================================================
#--GenericHandler--
name = "fshp"
setting_kwds = ("salt", "salt_size", "rounds", "variant")
checksum_chars = uh.PADDED_BASE64_CHARS
ident = u("{FSHP")
# checksum_size is property() that depends on variant
#--HasRawSalt--
default_salt_size = 16 # current passlib default, FSHP uses 8
min_salt_size = 0
max_salt_size = None
#--HasRounds--
# FIXME: should probably use different default rounds
# based on the variant. setting for default variant (sha256) for now.
default_rounds = 100000 # current passlib default, FSHP uses 4096
min_rounds = 1 # set by FSHP
max_rounds = 4294967295 # 32-bit integer limit - not set by FSHP
rounds_cost = "linear"
#--variants--
default_variant = 1
_variant_info = {
# variant: (hash name, digest size)
0: ("sha1", 20),
1: ("sha256", 32),
2: ("sha384", 48),
3: ("sha512", 64),
}
_variant_aliases = dict(
[(unicode(k),k) for k in _variant_info] +
[(v[0],k) for k,v in iteritems(_variant_info)]
)
#===================================================================
# instance attrs
#===================================================================
variant = None
#===================================================================
# init
#===================================================================
def __init__(self, variant=None, **kwds):
# NOTE: variant must be set first, since it controls checksum size, etc.
self.use_defaults = kwds.get("use_defaults") # load this early
self.variant = self._norm_variant(variant)
super(fshp, self).__init__(**kwds)
def _norm_variant(self, variant):
if variant is None:
if not self.use_defaults:
raise TypeError("no variant specified")
variant = self.default_variant
if isinstance(variant, bytes):
variant = variant.decode("ascii")
if isinstance(variant, unicode):
try:
variant = self._variant_aliases[variant]
except KeyError:
raise ValueError("invalid fshp variant")
if not isinstance(variant, int):
raise TypeError("fshp variant must be int or known alias")
if variant not in self._variant_info:
raise ValueError("invalid fshp variant")
return variant
@property
def checksum_alg(self):
return self._variant_info[self.variant][0]
@property
def checksum_size(self):
return self._variant_info[self.variant][1]
#===================================================================
# formatting
#===================================================================
_hash_regex = re.compile(u(r"""
^
\{FSHP
(\d+)\| # variant
(\d+)\| # salt size
(\d+)\} # rounds
([a-zA-Z0-9+/]+={0,3}) # digest
$"""), re.X)
@classmethod
def from_string(cls, hash):
hash = to_unicode(hash, "ascii", "hash")
m = cls._hash_regex.match(hash)
if not m:
raise uh.exc.InvalidHashError(cls)
variant, salt_size, rounds, data = m.group(1,2,3,4)
variant = int(variant)
salt_size = int(salt_size)
rounds = int(rounds)
try:
data = b64decode(data.encode("ascii"))
except TypeError:
raise uh.exc.MalformedHashError(cls)
salt = data[:salt_size]
chk = data[salt_size:]
return cls(salt=salt, checksum=chk, rounds=rounds, variant=variant)
@property
def _stub_checksum(self):
return b('\x00') * self.checksum_size
def to_string(self):
chk = self.checksum or self._stub_checksum
salt = self.salt
data = bascii_to_str(b64encode(salt+chk))
return "{FSHP%d|%d|%d}%s" % (self.variant, len(salt), self.rounds, data)
#===================================================================
# backend
#===================================================================
def _calc_checksum(self, secret):
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
# NOTE: for some reason, FSHP uses pbkdf1 with password & salt reversed.
# this has only a minimal impact on security,
# but it is worth noting this deviation.
return pbkdf1(
secret=self.salt,
salt=secret,
rounds=self.rounds,
keylen=self.checksum_size,
hash=self.checksum_alg,
)
#===================================================================
# eoc
#===================================================================
#=============================================================================
# eof
#=============================================================================

View File

@ -0,0 +1,270 @@
"""passlib.handlers.digests - plain hash digests
"""
#=============================================================================
# imports
#=============================================================================
# core
from base64 import b64encode, b64decode
from hashlib import md5, sha1
import logging; log = logging.getLogger(__name__)
import re
from warnings import warn
# site
# pkg
from passlib.handlers.misc import plaintext
from passlib.utils import to_native_str, unix_crypt_schemes, \
classproperty, to_unicode
from passlib.utils.compat import b, bytes, uascii_to_str, unicode, u
import passlib.utils.handlers as uh
# local
__all__ = [
"ldap_plaintext",
"ldap_md5",
"ldap_sha1",
"ldap_salted_md5",
"ldap_salted_sha1",
##"get_active_ldap_crypt_schemes",
"ldap_des_crypt",
"ldap_bsdi_crypt",
"ldap_md5_crypt",
"ldap_sha1_crypt"
"ldap_bcrypt",
"ldap_sha256_crypt",
"ldap_sha512_crypt",
]
#=============================================================================
# ldap helpers
#=============================================================================
class _Base64DigestHelper(uh.StaticHandler):
"helper for ldap_md5 / ldap_sha1"
# XXX: could combine this with hex digests in digests.py
ident = None # required - prefix identifier
_hash_func = None # required - hash function
_hash_regex = None # required - regexp to recognize hash
checksum_chars = uh.PADDED_BASE64_CHARS
@classproperty
def _hash_prefix(cls):
"tell StaticHandler to strip ident from checksum"
return cls.ident
def _calc_checksum(self, secret):
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
chk = self._hash_func(secret).digest()
return b64encode(chk).decode("ascii")
class _SaltedBase64DigestHelper(uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
"helper for ldap_salted_md5 / ldap_salted_sha1"
setting_kwds = ("salt", "salt_size")
checksum_chars = uh.PADDED_BASE64_CHARS
ident = None # required - prefix identifier
checksum_size = None # required
_hash_func = None # required - hash function
_hash_regex = None # required - regexp to recognize hash
_stub_checksum = None # required - default checksum to plug in
min_salt_size = max_salt_size = 4
# NOTE: openldap implementation uses 4 byte salt,
# but it's been reported (issue 30) that some servers use larger salts.
# the semi-related rfc3112 recommends support for up to 16 byte salts.
min_salt_size = 4
default_salt_size = 4
max_salt_size = 16
@classmethod
def from_string(cls, hash):
hash = to_unicode(hash, "ascii", "hash")
m = cls._hash_regex.match(hash)
if not m:
raise uh.exc.InvalidHashError(cls)
try:
data = b64decode(m.group("tmp").encode("ascii"))
except TypeError:
raise uh.exc.MalformedHashError(cls)
cs = cls.checksum_size
assert cs
return cls(checksum=data[:cs], salt=data[cs:])
def to_string(self):
data = (self.checksum or self._stub_checksum) + self.salt
hash = self.ident + b64encode(data).decode("ascii")
return uascii_to_str(hash)
def _calc_checksum(self, secret):
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
return self._hash_func(secret + self.salt).digest()
#=============================================================================
# implementations
#=============================================================================
class ldap_md5(_Base64DigestHelper):
"""This class stores passwords using LDAP's plain MD5 format, and follows the :ref:`password-hash-api`.
The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods have no optional keywords.
"""
name = "ldap_md5"
ident = u("{MD5}")
_hash_func = md5
_hash_regex = re.compile(u(r"^\{MD5\}(?P<chk>[+/a-zA-Z0-9]{22}==)$"))
class ldap_sha1(_Base64DigestHelper):
"""This class stores passwords using LDAP's plain SHA1 format, and follows the :ref:`password-hash-api`.
The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods have no optional keywords.
"""
name = "ldap_sha1"
ident = u("{SHA}")
_hash_func = sha1
_hash_regex = re.compile(u(r"^\{SHA\}(?P<chk>[+/a-zA-Z0-9]{27}=)$"))
class ldap_salted_md5(_SaltedBase64DigestHelper):
"""This class stores passwords using LDAP's salted MD5 format, and follows the :ref:`password-hash-api`.
It supports a 4-16 byte salt.
The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keyword:
:type salt: bytes
:param salt:
Optional salt string.
If not specified, one will be autogenerated (this is recommended).
If specified, it may be any 4-16 byte string.
:type salt_size: int
:param salt_size:
Optional number of bytes to use when autogenerating new salts.
Defaults to 4 bytes for compatibility with the LDAP spec,
but some systems use larger salts, and Passlib supports
any value between 4-16.
:type relaxed: bool
:param relaxed:
By default, providing an invalid value for one of the other
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
will be issued instead. Correctable errors include
``salt`` strings that are too long.
.. versionadded:: 1.6
.. versionchanged:: 1.6
This format now supports variable length salts, instead of a fix 4 bytes.
"""
name = "ldap_salted_md5"
ident = u("{SMD5}")
checksum_size = 16
_hash_func = md5
_hash_regex = re.compile(u(r"^\{SMD5\}(?P<tmp>[+/a-zA-Z0-9]{27,}={0,2})$"))
_stub_checksum = b('\x00') * 16
class ldap_salted_sha1(_SaltedBase64DigestHelper):
"""This class stores passwords using LDAP's salted SHA1 format, and follows the :ref:`password-hash-api`.
It supports a 4-16 byte salt.
The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keyword:
:type salt: bytes
:param salt:
Optional salt string.
If not specified, one will be autogenerated (this is recommended).
If specified, it may be any 4-16 byte string.
:type salt_size: int
:param salt_size:
Optional number of bytes to use when autogenerating new salts.
Defaults to 4 bytes for compatibility with the LDAP spec,
but some systems use larger salts, and Passlib supports
any value between 4-16.
:type relaxed: bool
:param relaxed:
By default, providing an invalid value for one of the other
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
will be issued instead. Correctable errors include
``salt`` strings that are too long.
.. versionadded:: 1.6
.. versionchanged:: 1.6
This format now supports variable length salts, instead of a fix 4 bytes.
"""
name = "ldap_salted_sha1"
ident = u("{SSHA}")
checksum_size = 20
_hash_func = sha1
_hash_regex = re.compile(u(r"^\{SSHA\}(?P<tmp>[+/a-zA-Z0-9]{32,}={0,2})$"))
_stub_checksum = b('\x00') * 20
class ldap_plaintext(plaintext):
"""This class stores passwords in plaintext, and follows the :ref:`password-hash-api`.
This class acts much like the generic :class:`!passlib.hash.plaintext` handler,
except that it will identify a hash only if it does NOT begin with the ``{XXX}`` identifier prefix
used by RFC2307 passwords.
The :meth:`~passlib.ifc.PasswordHash.encrypt`, :meth:`~passlib.ifc.PasswordHash.genhash`, and :meth:`~passlib.ifc.PasswordHash.verify` methods all require the
following additional contextual keyword:
:type encoding: str
:param encoding:
This controls the character encoding to use (defaults to ``utf-8``).
This encoding will be used to encode :class:`!unicode` passwords
under Python 2, and decode :class:`!bytes` hashes under Python 3.
.. versionchanged:: 1.6
The ``encoding`` keyword was added.
"""
# NOTE: this subclasses plaintext, since all it does differently
# is override identify()
name = "ldap_plaintext"
_2307_pat = re.compile(u(r"^\{\w+\}.*$"))
@classmethod
def identify(cls, hash):
# NOTE: identifies all strings EXCEPT those with {XXX} prefix
hash = uh.to_unicode_for_identify(hash)
return bool(hash) and cls._2307_pat.match(hash) is None
#=============================================================================
# {CRYPT} wrappers
# the following are wrappers around the base crypt algorithms,
# which add the ldap required {CRYPT} prefix
#=============================================================================
ldap_crypt_schemes = [ 'ldap_' + name for name in unix_crypt_schemes ]
def _init_ldap_crypt_handlers():
# NOTE: I don't like to implicitly modify globals() like this,
# but don't want to write out all these handlers out either :)
g = globals()
for wname in unix_crypt_schemes:
name = 'ldap_' + wname
g[name] = uh.PrefixWrapper(name, wname, prefix=u("{CRYPT}"), lazy=True)
del g
_init_ldap_crypt_handlers()
##_lcn_host = None
##def get_host_ldap_crypt_schemes():
## global _lcn_host
## if _lcn_host is None:
## from passlib.hosts import host_context
## schemes = host_context.schemes()
## _lcn_host = [
## "ldap_" + name
## for name in unix_crypt_names
## if name in schemes
## ]
## return _lcn_host
#=============================================================================
# eof
#=============================================================================

View File

@ -0,0 +1,333 @@
"""passlib.handlers.md5_crypt - md5-crypt algorithm"""
#=============================================================================
# imports
#=============================================================================
# core
from hashlib import md5
import re
import logging; log = logging.getLogger(__name__)
from warnings import warn
# site
# pkg
from passlib.utils import classproperty, h64, safe_crypt, test_crypt, repeat_string
from passlib.utils.compat import b, bytes, irange, unicode, u
import passlib.utils.handlers as uh
# local
__all__ = [
"md5_crypt",
"apr_md5_crypt",
]
#=============================================================================
# pure-python backend
#=============================================================================
_BNULL = b("\x00")
_MD5_MAGIC = b("$1$")
_APR_MAGIC = b("$apr1$")
# pre-calculated offsets used to speed up C digest stage (see notes below).
# sequence generated using the following:
##perms_order = "p,pp,ps,psp,sp,spp".split(",")
##def offset(i):
## key = (("p" if i % 2 else "") + ("s" if i % 3 else "") +
## ("p" if i % 7 else "") + ("" if i % 2 else "p"))
## return perms_order.index(key)
##_c_digest_offsets = [(offset(i), offset(i+1)) for i in range(0,42,2)]
_c_digest_offsets = (
(0, 3), (5, 1), (5, 3), (1, 2), (5, 1), (5, 3), (1, 3),
(4, 1), (5, 3), (1, 3), (5, 0), (5, 3), (1, 3), (5, 1),
(4, 3), (1, 3), (5, 1), (5, 2), (1, 3), (5, 1), (5, 3),
)
# map used to transpose bytes when encoding final digest
_transpose_map = (12, 6, 0, 13, 7, 1, 14, 8, 2, 15, 9, 3, 5, 10, 4, 11)
def _raw_md5_crypt(pwd, salt, use_apr=False):
"""perform raw md5-crypt calculation
this function provides a pure-python implementation of the internals
for the MD5-Crypt algorithms; it doesn't handle any of the
parsing/validation of the hash strings themselves.
:arg pwd: password chars/bytes to encrypt
:arg salt: salt chars to use
:arg use_apr: use apache variant
:returns:
encoded checksum chars
"""
# NOTE: regarding 'apr' format:
# really, apache? you had to invent a whole new "$apr1$" format,
# when all you did was change the ident incorporated into the hash?
# would love to find webpage explaining why just using a portable
# implementation of $1$ wasn't sufficient. *nothing else* was changed.
#===================================================================
# init & validate inputs
#===================================================================
# validate secret
# XXX: not sure what official unicode policy is, using this as default
if isinstance(pwd, unicode):
pwd = pwd.encode("utf-8")
assert isinstance(pwd, bytes), "pwd not unicode or bytes"
if _BNULL in pwd:
raise uh.exc.NullPasswordError(md5_crypt)
pwd_len = len(pwd)
# validate salt - should have been taken care of by caller
assert isinstance(salt, unicode), "salt not unicode"
salt = salt.encode("ascii")
assert len(salt) < 9, "salt too large"
# NOTE: spec says salts larger than 8 bytes should be truncated,
# instead of causing an error. this function assumes that's been
# taken care of by the handler class.
# load APR specific constants
if use_apr:
magic = _APR_MAGIC
else:
magic = _MD5_MAGIC
#===================================================================
# digest B - used as subinput to digest A
#===================================================================
db = md5(pwd + salt + pwd).digest()
#===================================================================
# digest A - used to initialize first round of digest C
#===================================================================
# start out with pwd + magic + salt
a_ctx = md5(pwd + magic + salt)
a_ctx_update = a_ctx.update
# add pwd_len bytes of b, repeating b as many times as needed.
a_ctx_update(repeat_string(db, pwd_len))
# add null chars & first char of password
# NOTE: this may have historically been a bug,
# where they meant to use db[0] instead of B_NULL,
# but the original code memclear'ed db,
# and now all implementations have to use this.
i = pwd_len
evenchar = pwd[:1]
while i:
a_ctx_update(_BNULL if i & 1 else evenchar)
i >>= 1
# finish A
da = a_ctx.digest()
#===================================================================
# digest C - for a 1000 rounds, combine A, S, and P
# digests in various ways; in order to burn CPU time.
#===================================================================
# NOTE: the original MD5-Crypt implementation performs the C digest
# calculation using the following loop:
#
##dc = da
##i = 0
##while i < rounds:
## tmp_ctx = md5(pwd if i & 1 else dc)
## if i % 3:
## tmp_ctx.update(salt)
## if i % 7:
## tmp_ctx.update(pwd)
## tmp_ctx.update(dc if i & 1 else pwd)
## dc = tmp_ctx.digest()
## i += 1
#
# The code Passlib uses (below) implements an equivalent algorithm,
# it's just been heavily optimized to pre-calculate a large number
# of things beforehand. It works off of a couple of observations
# about the original algorithm:
#
# 1. each round is a combination of 'dc', 'salt', and 'pwd'; and the exact
# combination is determined by whether 'i' a multiple of 2,3, and/or 7.
# 2. since lcm(2,3,7)==42, the series of combinations will repeat
# every 42 rounds.
# 3. even rounds 0-40 consist of 'hash(dc + round-specific-constant)';
# while odd rounds 1-41 consist of hash(round-specific-constant + dc)
#
# Using these observations, the following code...
# * calculates the round-specific combination of salt & pwd for each round 0-41
# * runs through as many 42-round blocks as possible (23)
# * runs through as many pairs of rounds as needed for remaining rounds (17)
# * this results in the required 42*23+2*17=1000 rounds required by md5_crypt.
#
# this cuts out a lot of the control overhead incurred when running the
# original loop 1000 times in python, resulting in ~20% increase in
# speed under CPython (though still 2x slower than glibc crypt)
# prepare the 6 combinations of pwd & salt which are needed
# (order of 'perms' must match how _c_digest_offsets was generated)
pwd_pwd = pwd+pwd
pwd_salt = pwd+salt
perms = [pwd, pwd_pwd, pwd_salt, pwd_salt+pwd, salt+pwd, salt+pwd_pwd]
# build up list of even-round & odd-round constants,
# and store in 21-element list as (even,odd) pairs.
data = [ (perms[even], perms[odd]) for even, odd in _c_digest_offsets]
# perform 23 blocks of 42 rounds each (for a total of 966 rounds)
dc = da
blocks = 23
while blocks:
for even, odd in data:
dc = md5(odd + md5(dc + even).digest()).digest()
blocks -= 1
# perform 17 more pairs of rounds (34 more rounds, for a total of 1000)
for even, odd in data[:17]:
dc = md5(odd + md5(dc + even).digest()).digest()
#===================================================================
# encode digest using appropriate transpose map
#===================================================================
return h64.encode_transposed_bytes(dc, _transpose_map).decode("ascii")
#=============================================================================
# handler
#=============================================================================
class _MD5_Common(uh.HasSalt, uh.GenericHandler):
"common code for md5_crypt and apr_md5_crypt"
#===================================================================
# class attrs
#===================================================================
# name - set in subclass
setting_kwds = ("salt", "salt_size")
# ident - set in subclass
checksum_size = 22
checksum_chars = uh.HASH64_CHARS
min_salt_size = 0
max_salt_size = 8
salt_chars = uh.HASH64_CHARS
#===================================================================
# methods
#===================================================================
@classmethod
def from_string(cls, hash):
salt, chk = uh.parse_mc2(hash, cls.ident, handler=cls)
return cls(salt=salt, checksum=chk)
def to_string(self):
return uh.render_mc2(self.ident, self.salt, self.checksum)
# _calc_checksum() - provided by subclass
#===================================================================
# eoc
#===================================================================
class md5_crypt(uh.HasManyBackends, _MD5_Common):
"""This class implements the MD5-Crypt password hash, and follows the :ref:`password-hash-api`.
It supports a variable-length salt.
The :meth:`~passlib.ifc.PasswordHash.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 0-8 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
:type salt_size: int
:param salt_size:
Optional number of characters to use when autogenerating new salts.
Defaults to 8, but can be any value between 0 and 8.
(This is mainly needed when generating Cisco-compatible hashes,
which require ``salt_size=4``).
:type relaxed: bool
:param relaxed:
By default, providing an invalid value for one of the other
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
will be issued instead. Correctable errors include
``salt`` strings that are too long.
.. versionadded:: 1.6
"""
#===================================================================
# class attrs
#===================================================================
name = "md5_crypt"
ident = u("$1$")
#===================================================================
# methods
#===================================================================
# FIXME: can't find definitive policy on how md5-crypt handles non-ascii.
# all backends currently coerce -> utf-8
backends = ("os_crypt", "builtin")
_has_backend_builtin = True
@classproperty
def _has_backend_os_crypt(cls):
return test_crypt("test", '$1$test$pi/xDtU5WFVRqYS6BMU8X/')
def _calc_checksum_builtin(self, secret):
return _raw_md5_crypt(secret, self.salt)
def _calc_checksum_os_crypt(self, secret):
config = self.ident + self.salt
hash = safe_crypt(secret, config)
if hash:
assert hash.startswith(config) and len(hash) == len(config) + 23
return hash[-22:]
else:
return self._calc_checksum_builtin(secret)
#===================================================================
# eoc
#===================================================================
class apr_md5_crypt(_MD5_Common):
"""This class implements the Apr-MD5-Crypt password hash, and follows the :ref:`password-hash-api`.
It supports a variable-length salt.
The :meth:`~passlib.ifc.PasswordHash.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 0-8 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
:type relaxed: bool
:param relaxed:
By default, providing an invalid value for one of the other
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
will be issued instead. Correctable errors include
``salt`` strings that are too long.
.. versionadded:: 1.6
"""
#===================================================================
# class attrs
#===================================================================
name = "apr_md5_crypt"
ident = u("$apr1$")
#===================================================================
# methods
#===================================================================
def _calc_checksum(self, secret):
return _raw_md5_crypt(secret, self.salt, use_apr=True)
#===================================================================
# eoc
#===================================================================
#=============================================================================
# eof
#=============================================================================

242
passlib/handlers/misc.py Normal file
View File

@ -0,0 +1,242 @@
"""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, consteq
from passlib.utils.compat import bytes, unicode, u, b, base_string_types
import passlib.utils.handlers as uh
# local
__all__ = [
"unix_disabled",
"unix_fallback",
"plaintext",
]
#=============================================================================
# handler
#=============================================================================
class unix_fallback(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 positive identify all hash strings.
* for security, newly encrypted passwords will 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 it's "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, base_string_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
@classmethod
def genhash(cls, secret, config):
# override default to preserve checksum
if config is None:
return cls.encrypt(secret)
else:
uh.validate_secret(secret)
self = cls.from_string(config)
self.checksum = self._calc_checksum(secret)
return self.to_string()
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, base_string_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.PasswordHash):
"""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.encrypt`
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 = ()
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 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"
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 encrypt(cls, secret, marker=None):
return cls.genhash(secret, None, marker)
@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 genconfig(cls):
return None
@classmethod
def genhash(cls, secret, config, marker=None):
uh.validate_secret(secret)
if config is not None and not cls.identify(config): # handles typecheck
raise uh.exc.InvalidHashError(cls)
if config:
# we want to preserve the existing str,
# since it might contain a disabled password hash ("!" + hash)
return to_native_str(config, param="config")
# if None or empty string, replace with marker
if marker:
if not cls.identify(marker):
raise ValueError("invalid marker: %r" % marker)
else:
marker = cls.default_marker
assert marker and cls.identify(marker)
return to_native_str(marker, param="marker")
class plaintext(uh.PasswordHash):
"""This class stores passwords in plaintext, and follows the :ref:`password-hash-api`.
The :meth:`~passlib.ifc.PasswordHash.encrypt`, :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, base_string_types):
return True
else:
raise uh.exc.ExpectedStringError(hash, "hash")
@classmethod
def encrypt(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 consteq(cls.encrypt(secret, encoding), hash)
@classmethod
def genconfig(cls):
return None
@classmethod
def genhash(cls, secret, hash, encoding=None):
if hash is not None and not cls.identify(hash):
raise uh.exc.InvalidHashError(cls)
return cls.encrypt(secret, encoding)
#=============================================================================
# eof
#=============================================================================

246
passlib/handlers/mssql.py Normal file
View File

@ -0,0 +1,246 @@
"""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 b, bytes, 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.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept 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
_stub_checksum = b("\x00") * 40
#===================================================================
# 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 or self._stub_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.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept 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
_stub_checksum = b("\x00") * 20
#===================================================================
# 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 or self._stub_checksum)
# raw bytes format - BIDENT2 + raw
return "0x0100" + bascii_to_str(hexlify(raw)).upper()
def _calc_checksum(self, secret):
if isinstance(secret, bytes):
secret = secret.decode("utf-8")
return _raw_mssql(secret, self.salt)
#===================================================================
# eoc
#===================================================================
#=============================================================================
# eof
#=============================================================================

128
passlib/handlers/mysql.py Normal file
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 b, bascii_to_str, bytes, 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.encrypt` 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.encrypt` 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
#=============================================================================

175
passlib/handlers/oracle.py Normal file
View File

@ -0,0 +1,175 @@
"""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__)
from warnings import warn
# site
# pkg
from passlib.utils import to_unicode, to_native_str, xor_bytes
from passlib.utils.compat import b, bytes, bascii_to_str, irange, u, \
uascii_to_str, unicode, str_to_uascii
from passlib.utils.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.encrypt`, :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.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 20 hexidecimal 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
_stub_checksum = u('0') * 40
#--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 or self._stub_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
#=============================================================================

490
passlib/handlers/pbkdf2.py Normal file
View File

@ -0,0 +1,490 @@
"""passlib.handlers.pbkdf - PBKDF2 based hashes"""
#=============================================================================
# imports
#=============================================================================
# core
from binascii import hexlify, unhexlify
from base64 import b64encode, b64decode
import re
import logging; log = logging.getLogger(__name__)
from warnings import warn
# site
# pkg
from passlib.utils import ab64_decode, ab64_encode, to_unicode
from passlib.utils.compat import b, bytes, str_to_bascii, u, uascii_to_str, unicode
from passlib.utils.pbkdf2 import pbkdf2
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
min_salt_size = 0
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--
_prf = None # subclass specified prf identifier
# 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, withchk=True):
salt = ab64_encode(self.salt).decode("ascii")
if withchk and self.checksum:
chk = ab64_encode(self.checksum).decode("ascii")
else:
chk = None
return uh.render_mc3(self.ident, self.rounds, salt, chk)
def _calc_checksum(self, secret):
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
return pbkdf2(secret, self.salt, self.rounds, self.checksum_size, self._prf)
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,)
prf = "hmac-%s" % (hash_name,)
base = Pbkdf2DigestHandler
return type(name, (base,), dict(
__module__=module, # so ABCMeta won't clobber it.
name=name,
ident=ident,
_prf = prf,
default_rounds=rounds,
checksum_size=digest_size,
encoded_checksum_size=(digest_size*4+2)//3,
__doc__="""This class implements a generic ``PBKDF2-%(prf)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.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept 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 16 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(prf=prf.upper(), dsc=base.default_salt_size, dr=rounds)
))
#------------------------------------------------------------------------
# derived handlers
#------------------------------------------------------------------------
pbkdf2_sha1 = create_pbkdf2_hash("sha1", 20, 60000, ident=u("$pbkdf2$"))
pbkdf2_sha256 = create_pbkdf2_hash("sha256", 32, 20000)
pbkdf2_sha512 = create_pbkdf2_hash("sha512", 64, 19000)
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.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept 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$")
# 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
min_salt_size = 0
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, withchk=True):
salt = b64encode(self.salt, CTA_ALTCHARS).decode("ascii")
if withchk and self.checksum:
chk = b64encode(self.checksum, CTA_ALTCHARS).decode("ascii")
else:
chk = None
return uh.render_mc3(self.ident, self.rounds, salt, chk, rounds_base=16)
#===================================================================
# backend
#===================================================================
def _calc_checksum(self, secret):
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
return pbkdf2(secret, self.salt, self.rounds, 20, "hmac-sha1")
#===================================================================
# 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.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept 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$")
# 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
min_salt_size = 0
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, withchk=True):
rounds = self.rounds
if rounds == 400:
rounds = None # omit rounds measurement if == 400
return uh.render_mc3(self.ident, rounds, self.salt,
checksum=self.checksum if withchk else None,
rounds_base=16)
#===================================================================
# backend
#===================================================================
def _calc_checksum(self, secret):
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
salt = str_to_bascii(self.to_string(withchk=False))
result = pbkdf2(secret, salt, self.rounds, 24, "hmac-sha1")
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.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keyword:
: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
_stub_checksum = b("\x00") * 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 or self._stub_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
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
# crowd seems to use a fixed number of rounds.
return pbkdf2(secret, self.salt, 10000, 32, "hmac-sha1")
#=============================================================================
# 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.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept 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.")
# 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
min_salt_size = 0
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, withchk=True):
salt = hexlify(self.salt).decode("ascii").upper()
if withchk and self.checksum:
chk = hexlify(self.checksum).decode("ascii").upper()
else:
chk = None
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
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
return pbkdf2(secret, self.salt, self.rounds, 64, "hmac-sha512")
#=============================================================================
# eof
#=============================================================================

137
passlib/handlers/phpass.py Normal file
View File

@ -0,0 +1,137 @@
"""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 re
import logging; log = logging.getLogger(__name__)
from warnings import warn
# site
# pkg
from passlib.utils import h64
from passlib.utils.compat import b, bytes, 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.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 8 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
:type rounds: int
:param rounds:
Optional number of rounds to use.
Defaults to 17, 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 it's 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 = 17
min_rounds = 7
max_rounds = 30
rounds_cost = "log2"
#--HasManyIdents--
default_ident = u("$P$")
ident_values = [u("$P$"), u("$H$")]
ident_aliases = {u("P"):u("$P$"), u("H"):u("$H$")}
#===================================================================
# formatting
#===================================================================
#$P$9IQRaTwmfeRo7ud9Fh4E2PdI0S3r.L0
# $P$
# 9
# IQRaTwmf
# eRo7ud9Fh4E2PdI0S3r.L0
@classmethod
def from_string(cls, hash):
ident, data = cls._parse_ident(hash)
rounds, salt, chk = data[0], data[1:9], data[9:]
return cls(
ident=ident,
rounds=h64.decode_int6(rounds.encode("ascii")),
salt=salt,
checksum=chk or None,
)
def to_string(self):
hash = u("%s%s%s%s") % (self.ident,
h64.encode_int6(self.rounds).decode("ascii"),
self.salt,
self.checksum or u(''))
return uascii_to_str(hash)
#===================================================================
# backend
#===================================================================
def _calc_checksum(self, secret):
# FIXME: can't find definitive policy on how phpass handles non-ascii.
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
real_rounds = 1<<self.rounds
result = md5(self.salt.encode("ascii") + secret).digest()
r = 0
while r < real_rounds:
result = md5(result + secret).digest()
r += 1
return h64.encode_bytes(result).decode("ascii")
#===================================================================
# eoc
#===================================================================
#=============================================================================
# eof
#=============================================================================

View File

@ -0,0 +1,57 @@
"""passlib.handlers.postgres_md5 - MD5-based algorithm used by Postgres for pg_shadow table"""
#=============================================================================
# imports
#=============================================================================
# core
from hashlib import md5
import re
import logging; log = logging.getLogger(__name__)
from warnings import warn
# site
# pkg
from passlib.utils import to_bytes
from passlib.utils.compat import b, bytes, 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.encrypt`, :meth:`~passlib.ifc.PasswordHash.genhash`, and :meth:`~passlib.ifc.PasswordHash.verify` methods all require the
following additional contextual keywords:
:type user: str
:param user: name of postgres user account this password is associated with.
"""
#===================================================================
# algorithm information
#===================================================================
name = "postgres_md5"
_hash_prefix = u("md5")
checksum_chars = uh.HEX_CHARS
checksum_size = 32
#===================================================================
# primary interface
#===================================================================
def _calc_checksum(self, secret):
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
user = to_bytes(self.user, "utf-8", param="user")
return str_to_uascii(md5(secret + user).hexdigest())
#===================================================================
# eoc
#===================================================================
#=============================================================================
# eof
#=============================================================================

View File

@ -0,0 +1,29 @@
"""passlib.handlers.roundup - Roundup issue tracker hashes"""
#=============================================================================
# imports
#=============================================================================
# core
import logging; log = logging.getLogger(__name__)
# site
# pkg
import passlib.utils.handlers as uh
from passlib.utils.compat import u
# local
__all__ = [
"roundup_plaintext",
"ldap_hex_md5",
"ldap_hex_sha1",
]
#=============================================================================
#
#=============================================================================
roundup_plaintext = uh.PrefixWrapper("roundup_plaintext", "plaintext",
prefix=u("{plaintext}"), lazy=True)
# NOTE: these are here because they're currently only known to be used by roundup
ldap_hex_md5 = uh.PrefixWrapper("ldap_hex_md5", "hex_md5", u("{MD5}"), lazy=True)
ldap_hex_sha1 = uh.PrefixWrapper("ldap_hex_sha1", "hex_sha1", u("{SHA}"), lazy=True)
#=============================================================================
# eof
#=============================================================================

576
passlib/handlers/scram.py Normal file
View File

@ -0,0 +1,576 @@
"""passlib.handlers.scram - hash for SCRAM credential storage"""
#=============================================================================
# imports
#=============================================================================
# core
from binascii import hexlify, unhexlify
from base64 import b64encode, b64decode
import hashlib
import re
import logging; log = logging.getLogger(__name__)
from warnings import warn
# site
# pkg
from passlib.exc import PasslibHashWarning
from passlib.utils import ab64_decode, ab64_encode, consteq, saslprep, \
to_native_str, xor_bytes, splitcomma
from passlib.utils.compat import b, bytes, bascii_to_str, iteritems, \
PY3, u, unicode
from passlib.utils.pbkdf2 import pbkdf2, get_prf, 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.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept 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 20000, 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
min_salt_size = 0
max_salt_size = 1024
#--HasRounds--
default_rounds = 20000
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.utils.pbkdf2.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; see :func:`~passlib.utils.pbkdf2.norm_hash_name`
for possible values.
: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")
password = saslprep(password).encode("utf-8")
if not isinstance(salt, bytes):
raise TypeError("salt must be bytes")
if rounds < 1:
raise ValueError("rounds must be >= 1")
alg = norm_hash_name(alg, "hashlib")
return pbkdf2(password, salt, rounds, None, "hmac-" + alg)
#===================================================================
# 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, withchk=True):
salt = bascii_to_str(ab64_encode(self.salt))
chkmap = self.checksum
if withchk and chkmap:
chk_str = ",".join(
"%s=%s" % (alg, bascii_to_str(ab64_encode(chkmap[alg])))
for alg in self.algs
)
else:
chk_str = ",".join(self.algs)
return '$scram$%d$%s$%s' % (self.rounds, salt, chk_str)
#===================================================================
# init
#===================================================================
def __init__(self, algs=None, **kwds):
super(scram, self).__init__(**kwds)
self.algs = self._norm_algs(algs)
def _norm_checksum(self, checksum):
if checksum is None:
return None
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
def _norm_algs(self, algs):
"normalize algs parameter"
# determine default algs value
if algs is None:
# derive algs list from checksum (if present).
chk = self.checksum
if chk is not None:
return sorted(chk)
elif self.use_defaults:
return list(self.default_algs)
else:
raise TypeError("no algs list specified")
elif self.checksum is not None:
raise RuntimeError("checksum & algs kwds are mutually exclusive")
# parse args value
if isinstance(algs, str):
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
#===================================================================
# digest methods
#===================================================================
@classmethod
def _bind_needs_update(cls, **settings):
"generate a deprecation detector for CryptContext to use"
# generate deprecation hook which marks hashes as deprecated
# if they don't support a superset of current algs.
algs = frozenset(cls(use_defaults=True, **settings).algs)
def detector(hash, secret):
return not algs.issubset(cls.from_string(hash).algs)
return detector
def _calc_checksum(self, secret, alg=None):
rounds = self.rounds
salt = self.salt
hash = self.derive_digest
if alg:
# if requested, generate digest for specific alg
return hash(secret, salt, rounds, alg)
else:
# by default, return dict containing digests for all algs
return dict(
(alg, hash(secret, salt, rounds, alg))
for alg in self.algs
)
@classmethod
def verify(cls, secret, hash, full=False):
uh.validate_secret(secret)
self = cls.from_string(hash)
chkmap = self.checksum
if not chkmap:
raise ValueError("expected %s hash, got %s config string instead" %
(cls.name, cls.name))
# NOTE: to make the verify method efficient, we just calculate hash
# of shortest digest by default. apps can pass in "full=True" to
# check entire hash for consistency.
if full:
correct = failed = False
for alg, digest in iteritems(chkmap):
other = self._calc_checksum(secret, alg)
# NOTE: could do this length check in norm_algs(),
# but don't need to be that strict, and want to be able
# to parse hashes containing algs not supported by platform.
# it's fine if we fail here though.
if len(digest) != len(other):
raise ValueError("mis-sized %s digest in scram hash: %r != %r"
% (alg, len(digest), len(other)))
if consteq(other, digest):
correct = True
else:
failed = True
if correct and failed:
raise ValueError("scram hash verified inconsistently, "
"may be corrupted")
else:
return correct
else:
# XXX: should this just always use sha1 hash? would be faster.
# otherwise only verify against one hash, pick one w/ best security.
for alg in self._verify_algs:
if alg in chkmap:
other = self._calc_checksum(secret, alg)
return consteq(other, chkmap[alg])
# there should always be sha-1 at the very least,
# or something went wrong inside _norm_algs()
raise AssertionError("sha-1 digest not found!")
#===================================================================
#
#===================================================================
#=============================================================================
# code used for testing scram against protocol examples during development.
#=============================================================================
##def _test_reference_scram():
## "quick hack testing scram reference vectors"
## # NOTE: "n,," is GS2 header - see https://tools.ietf.org/html/rfc5801
## from passlib.utils.compat import print_
##
## engine = _scram_engine(
## alg="sha-1",
## salt='QSXCR+Q6sek8bf92'.decode("base64"),
## rounds=4096,
## password=u("pencil"),
## )
## print_(engine.digest.encode("base64").rstrip())
##
## msg = engine.format_auth_msg(
## username="user",
## client_nonce = "fyko+d2lbbFgONRv9qkxdawL",
## server_nonce = "3rfcNHYJY1ZVvWVs7j",
## header='c=biws',
## )
##
## cp = engine.get_encoded_client_proof(msg)
## assert cp == "v0X8v3Bz2T0CJGbJQyF0X+HI4Ts=", cp
##
## ss = engine.get_encoded_server_sig(msg)
## assert ss == "rmF9pqV8S7suAoZWja4dJRkFsKQ=", ss
##
##class _scram_engine(object):
## """helper class for verifying scram hash behavior
## against SCRAM protocol examples. not officially part of Passlib.
##
## takes in alg, salt, rounds, and a digest or password.
##
## can calculate the various keys & messages of the scram protocol.
##
## """
## #=========================================================
## # init
## #=========================================================
##
## @classmethod
## def from_string(cls, hash, alg):
## "create record from scram hash, for given alg"
## return cls(alg, *scram.extract_digest_info(hash, alg))
##
## def __init__(self, alg, salt, rounds, digest=None, password=None):
## self.alg = norm_hash_name(alg)
## self.salt = salt
## self.rounds = rounds
## self.password = password
## if password:
## data = scram.derive_digest(password, salt, rounds, alg)
## if digest and data != digest:
## raise ValueError("password doesn't match digest")
## else:
## digest = data
## elif not digest:
## raise TypeError("must provide password or digest")
## self.digest = digest
##
## #=========================================================
## # frontend methods
## #=========================================================
## def get_hash(self, data):
## "return hash of raw data"
## return hashlib.new(iana_to_hashlib(self.alg), data).digest()
##
## def get_client_proof(self, msg):
## "return client proof of specified auth msg text"
## return xor_bytes(self.client_key, self.get_client_sig(msg))
##
## def get_encoded_client_proof(self, msg):
## return self.get_client_proof(msg).encode("base64").rstrip()
##
## def get_client_sig(self, msg):
## "return client signature of specified auth msg text"
## return self.get_hmac(self.stored_key, msg)
##
## def get_server_sig(self, msg):
## "return server signature of specified auth msg text"
## return self.get_hmac(self.server_key, msg)
##
## def get_encoded_server_sig(self, msg):
## return self.get_server_sig(msg).encode("base64").rstrip()
##
## def format_server_response(self, client_nonce, server_nonce):
## return 'r={client_nonce}{server_nonce},s={salt},i={rounds}'.format(
## client_nonce=client_nonce,
## server_nonce=server_nonce,
## rounds=self.rounds,
## salt=self.encoded_salt,
## )
##
## def format_auth_msg(self, username, client_nonce, server_nonce,
## header='c=biws'):
## return (
## 'n={username},r={client_nonce}'
## ','
## 'r={client_nonce}{server_nonce},s={salt},i={rounds}'
## ','
## '{header},r={client_nonce}{server_nonce}'
## ).format(
## username=username,
## client_nonce=client_nonce,
## server_nonce=server_nonce,
## salt=self.encoded_salt,
## rounds=self.rounds,
## header=header,
## )
##
## #=========================================================
## # helpers to calculate & cache constant data
## #=========================================================
## def _calc_get_hmac(self):
## return get_prf("hmac-" + iana_to_hashlib(self.alg))[0]
##
## def _calc_client_key(self):
## return self.get_hmac(self.digest, b("Client Key"))
##
## def _calc_stored_key(self):
## return self.get_hash(self.client_key)
##
## def _calc_server_key(self):
## return self.get_hmac(self.digest, b("Server Key"))
##
## def _calc_encoded_salt(self):
## return self.salt.encode("base64").rstrip()
##
## #=========================================================
## # hacks for calculated attributes
## #=========================================================
##
## def __getattr__(self, attr):
## if not attr.startswith("_"):
## f = getattr(self, "_calc_" + attr, None)
## if f:
## value = f()
## setattr(self, attr, value)
## return value
## raise AttributeError("attribute not found")
##
## def __dir__(self):
## cdir = dir(self.__class__)
## attrs = set(cdir)
## attrs.update(self.__dict__)
## attrs.update(attr[6:] for attr in cdir
## if attr.startswith("_calc_"))
## return sorted(attrs)
## #=========================================================
## # eoc
## #=========================================================
#=============================================================================
# eof
#=============================================================================

View File

@ -0,0 +1,150 @@
"""passlib.handlers.sha1_crypt
"""
#=============================================================================
# imports
#=============================================================================
# core
from hmac import new as hmac
from hashlib import sha1
import re
import logging; log = logging.getLogger(__name__)
from warnings import warn
# site
# pkg
from passlib.utils import classproperty, h64, safe_crypt, test_crypt
from passlib.utils.compat import b, bytes, u, uascii_to_str, unicode
from passlib.utils.pbkdf2 import get_prf
import passlib.utils.handlers as uh
# local
__all__ = [
]
#=============================================================================
# sha1-crypt
#=============================================================================
_hmac_sha1 = get_prf("hmac-sha1")[0]
_BNULL = b('\x00')
class sha1_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler):
"""This class implements the SHA1-Crypt password hash, and follows the :ref:`password-hash-api`.
It supports a variable-length salt, and a variable number of rounds.
The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
:type salt: str
:param salt:
Optional salt string.
If not specified, an 8 character one will be autogenerated (this is recommended).
If specified, it must be 0-64 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
:type salt_size: int
:param salt_size:
Optional number of bytes to use when autogenerating new salts.
Defaults to 8 bytes, but can be any value between 0 and 64.
:type rounds: int
:param rounds:
Optional number of rounds to use.
Defaults to 64000, must be between 1 and 4294967295, inclusive.
:type relaxed: bool
:param relaxed:
By default, providing an invalid value for one of the other
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
will be issued instead. Correctable errors include ``rounds``
that are too small or too large, and ``salt`` strings that are too long.
.. versionadded:: 1.6
"""
#===================================================================
# class attrs
#===================================================================
#--GenericHandler--
name = "sha1_crypt"
setting_kwds = ("salt", "salt_size", "rounds")
ident = u("$sha1$")
checksum_size = 28
checksum_chars = uh.HASH64_CHARS
#--HasSalt--
default_salt_size = 8
min_salt_size = 0
max_salt_size = 64
salt_chars = uh.HASH64_CHARS
#--HasRounds--
default_rounds = 64000 # current passlib default
min_rounds = 1 # really, this should be higher.
max_rounds = 4294967295 # 32-bit integer limit
rounds_cost = "linear"
#===================================================================
# formatting
#===================================================================
@classmethod
def from_string(cls, hash):
rounds, salt, chk = uh.parse_mc3(hash, cls.ident, handler=cls)
return cls(rounds=rounds, salt=salt, checksum=chk)
def to_string(self, config=False):
chk = None if config else self.checksum
return uh.render_mc3(self.ident, self.rounds, self.salt, chk)
#===================================================================
# backend
#===================================================================
backends = ("os_crypt", "builtin")
_has_backend_builtin = True
@classproperty
def _has_backend_os_crypt(cls):
return test_crypt("test", '$sha1$1$Wq3GL2Vp$C8U25GvfHS8qGHim'
'ExLaiSFlGkAe')
def _calc_checksum_builtin(self, secret):
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
if _BNULL in secret:
raise uh.exc.NullPasswordError(self)
rounds = self.rounds
# NOTE: this seed value is NOT the same as the config string
result = (u("%s$sha1$%s") % (self.salt, rounds)).encode("ascii")
# NOTE: this algorithm is essentially PBKDF1, modified to use HMAC.
r = 0
while r < rounds:
result = _hmac_sha1(secret, result)
r += 1
return h64.encode_transposed_bytes(result, self._chk_offsets).decode("ascii")
_chk_offsets = [
2,1,0,
5,4,3,
8,7,6,
11,10,9,
14,13,12,
17,16,15,
0,19,18,
]
def _calc_checksum_os_crypt(self, secret):
config = self.to_string(config=True)
hash = safe_crypt(secret, config)
if hash:
assert hash.startswith(config) and len(hash) == len(config) + 29
return hash[-28:]
else:
return self._calc_checksum_builtin(secret)
#===================================================================
# eoc
#===================================================================
#=============================================================================
# eof
#=============================================================================

View File

@ -0,0 +1,486 @@
"""passlib.handlers.sha2_crypt - SHA256-Crypt / SHA512-Crypt"""
#=============================================================================
# imports
#=============================================================================
# core
import hashlib
import logging; log = logging.getLogger(__name__)
from warnings import warn
# site
# pkg
from passlib.utils import classproperty, h64, safe_crypt, test_crypt, \
repeat_string, to_unicode
from passlib.utils.compat import b, bytes, byte_elem_value, irange, u, \
uascii_to_str, unicode
import passlib.utils.handlers as uh
# local
__all__ = [
"sha512_crypt",
"sha256_crypt",
]
#=============================================================================
# pure-python backend, used by both sha256_crypt & sha512_crypt
# when crypt.crypt() backend is not available.
#=============================================================================
_BNULL = b('\x00')
# pre-calculated offsets used to speed up C digest stage (see notes below).
# sequence generated using the following:
##perms_order = "p,pp,ps,psp,sp,spp".split(",")
##def offset(i):
## key = (("p" if i % 2 else "") + ("s" if i % 3 else "") +
## ("p" if i % 7 else "") + ("" if i % 2 else "p"))
## return perms_order.index(key)
##_c_digest_offsets = [(offset(i), offset(i+1)) for i in range(0,42,2)]
_c_digest_offsets = (
(0, 3), (5, 1), (5, 3), (1, 2), (5, 1), (5, 3), (1, 3),
(4, 1), (5, 3), (1, 3), (5, 0), (5, 3), (1, 3), (5, 1),
(4, 3), (1, 3), (5, 1), (5, 2), (1, 3), (5, 1), (5, 3),
)
# map used to transpose bytes when encoding final sha256_crypt digest
_256_transpose_map = (
20, 10, 0, 11, 1, 21, 2, 22, 12, 23, 13, 3, 14, 4, 24, 5,
25, 15, 26, 16, 6, 17, 7, 27, 8, 28, 18, 29, 19, 9, 30, 31,
)
# map used to transpose bytes when encoding final sha512_crypt digest
_512_transpose_map = (
42, 21, 0, 1, 43, 22, 23, 2, 44, 45, 24, 3, 4, 46, 25, 26,
5, 47, 48, 27, 6, 7, 49, 28, 29, 8, 50, 51, 30, 9, 10, 52,
31, 32, 11, 53, 54, 33, 12, 13, 55, 34, 35, 14, 56, 57, 36, 15,
16, 58, 37, 38, 17, 59, 60, 39, 18, 19, 61, 40, 41, 20, 62, 63,
)
def _raw_sha2_crypt(pwd, salt, rounds, use_512=False):
"""perform raw sha256-crypt / sha512-crypt
this function provides a pure-python implementation of the internals
for the SHA256-Crypt and SHA512-Crypt algorithms; it doesn't
handle any of the parsing/validation of the hash strings themselves.
:arg pwd: password chars/bytes to encrypt
:arg salt: salt chars to use
:arg rounds: linear rounds cost
:arg use_512: use sha512-crypt instead of sha256-crypt mode
:returns:
encoded checksum chars
"""
#===================================================================
# init & validate inputs
#===================================================================
# validate secret
if isinstance(pwd, unicode):
# XXX: not sure what official unicode policy is, using this as default
pwd = pwd.encode("utf-8")
assert isinstance(pwd, bytes)
if _BNULL in pwd:
raise uh.exc.NullPasswordError(sha512_crypt if use_512 else sha256_crypt)
pwd_len = len(pwd)
# validate rounds
assert 1000 <= rounds <= 999999999, "invalid rounds"
# NOTE: spec says out-of-range rounds should be clipped, instead of
# causing an error. this function assumes that's been taken care of
# by the handler class.
# validate salt
assert isinstance(salt, unicode), "salt not unicode"
salt = salt.encode("ascii")
salt_len = len(salt)
assert salt_len < 17, "salt too large"
# NOTE: spec says salts larger than 16 bytes should be truncated,
# instead of causing an error. this function assumes that's been
# taken care of by the handler class.
# load sha256/512 specific constants
if use_512:
hash_const = hashlib.sha512
hash_len = 64
transpose_map = _512_transpose_map
else:
hash_const = hashlib.sha256
hash_len = 32
transpose_map = _256_transpose_map
#===================================================================
# digest B - used as subinput to digest A
#===================================================================
db = hash_const(pwd + salt + pwd).digest()
#===================================================================
# digest A - used to initialize first round of digest C
#===================================================================
# start out with pwd + salt
a_ctx = hash_const(pwd + salt)
a_ctx_update = a_ctx.update
# add pwd_len bytes of b, repeating b as many times as needed.
a_ctx_update(repeat_string(db, pwd_len))
# for each bit in pwd_len: add b if it's 1, or pwd if it's 0
i = pwd_len
while i:
a_ctx_update(db if i & 1 else pwd)
i >>= 1
# finish A
da = a_ctx.digest()
#===================================================================
# digest P from password - used instead of password itself
# when calculating digest C.
#===================================================================
if pwd_len < 64:
# method this is faster under python, but uses O(pwd_len**2) memory
# so we don't use it for larger passwords, to avoid a potential DOS.
dp = repeat_string(hash_const(pwd * pwd_len).digest(), pwd_len)
else:
tmp_ctx = hash_const(pwd)
tmp_ctx_update = tmp_ctx.update
i = pwd_len-1
while i:
tmp_ctx_update(pwd)
i -= 1
dp = repeat_string(tmp_ctx.digest(), pwd_len)
assert len(dp) == pwd_len
#===================================================================
# digest S - used instead of salt itself when calculating digest C
#===================================================================
ds = hash_const(salt * (16 + byte_elem_value(da[0]))).digest()[:salt_len]
assert len(ds) == salt_len, "salt_len somehow > hash_len!"
#===================================================================
# digest C - for a variable number of rounds, combine A, S, and P
# digests in various ways; in order to burn CPU time.
#===================================================================
# NOTE: the original SHA256/512-Crypt specification performs the C digest
# calculation using the following loop:
#
##dc = da
##i = 0
##while i < rounds:
## tmp_ctx = hash_const(dp if i & 1 else dc)
## if i % 3:
## tmp_ctx.update(ds)
## if i % 7:
## tmp_ctx.update(dp)
## tmp_ctx.update(dc if i & 1 else dp)
## dc = tmp_ctx.digest()
## i += 1
#
# The code Passlib uses (below) implements an equivalent algorithm,
# it's just been heavily optimized to pre-calculate a large number
# of things beforehand. It works off of a couple of observations
# about the original algorithm:
#
# 1. each round is a combination of 'dc', 'ds', and 'dp'; determined
# by the whether 'i' a multiple of 2,3, and/or 7.
# 2. since lcm(2,3,7)==42, the series of combinations will repeat
# every 42 rounds.
# 3. even rounds 0-40 consist of 'hash(dc + round-specific-constant)';
# while odd rounds 1-41 consist of hash(round-specific-constant + dc)
#
# Using these observations, the following code...
# * calculates the round-specific combination of ds & dp for each round 0-41
# * runs through as many 42-round blocks as possible
# * runs through as many pairs of rounds as possible for remaining rounds
# * performs once last round if the total rounds should be odd.
#
# this cuts out a lot of the control overhead incurred when running the
# original loop 40,000+ times in python, resulting in ~20% increase in
# speed under CPython (though still 2x slower than glibc crypt)
# prepare the 6 combinations of ds & dp which are needed
# (order of 'perms' must match how _c_digest_offsets was generated)
dp_dp = dp+dp
dp_ds = dp+ds
perms = [dp, dp_dp, dp_ds, dp_ds+dp, ds+dp, ds+dp_dp]
# build up list of even-round & odd-round constants,
# and store in 21-element list as (even,odd) pairs.
data = [ (perms[even], perms[odd]) for even, odd in _c_digest_offsets]
# perform as many full 42-round blocks as possible
dc = da
blocks, tail = divmod(rounds, 42)
while blocks:
for even, odd in data:
dc = hash_const(odd + hash_const(dc + even).digest()).digest()
blocks -= 1
# perform any leftover rounds
if tail:
# perform any pairs of rounds
pairs = tail>>1
for even, odd in data[:pairs]:
dc = hash_const(odd + hash_const(dc + even).digest()).digest()
# if rounds was odd, do one last round (since we started at 0,
# last round will be an even-numbered round)
if tail & 1:
dc = hash_const(dc + data[pairs][0]).digest()
#===================================================================
# encode digest using appropriate transpose map
#===================================================================
return h64.encode_transposed_bytes(dc, transpose_map).decode("ascii")
#=============================================================================
# handlers
#=============================================================================
_UROUNDS = u("rounds=")
_UDOLLAR = u("$")
_UZERO = u("0")
class _SHA2_Common(uh.HasManyBackends, uh.HasRounds, uh.HasSalt,
uh.GenericHandler):
"class containing common code shared by sha256_crypt & sha512_crypt"
#===================================================================
# class attrs
#===================================================================
# name - set by subclass
setting_kwds = ("salt", "rounds", "implicit_rounds", "salt_size")
# ident - set by subclass
checksum_chars = uh.HASH64_CHARS
# checksum_size - set by subclass
min_salt_size = 0
max_salt_size = 16
salt_chars = uh.HASH64_CHARS
min_rounds = 1000 # bounds set by spec
max_rounds = 999999999 # bounds set by spec
rounds_cost = "linear"
_cdb_use_512 = False # flag for _calc_digest_builtin()
_rounds_prefix = None # ident + _UROUNDS
#===================================================================
# methods
#===================================================================
implicit_rounds = False
def __init__(self, implicit_rounds=None, **kwds):
super(_SHA2_Common, self).__init__(**kwds)
# if user calls encrypt() w/ 5000 rounds, default to compact form.
if implicit_rounds is None:
implicit_rounds = (self.use_defaults and self.rounds == 5000)
self.implicit_rounds = implicit_rounds
@classmethod
def from_string(cls, hash):
# basic format this parses -
# $5$[rounds=<rounds>$]<salt>[$<checksum>]
# TODO: this *could* use uh.parse_mc3(), except that the rounds
# portion has a slightly different grammar.
# convert to unicode, check for ident prefix, split on dollar signs.
hash = to_unicode(hash, "ascii", "hash")
ident = cls.ident
if not hash.startswith(ident):
raise uh.exc.InvalidHashError(cls)
assert len(ident) == 3
parts = hash[3:].split(_UDOLLAR)
# extract rounds value
if parts[0].startswith(_UROUNDS):
assert len(_UROUNDS) == 7
rounds = parts.pop(0)[7:]
if rounds.startswith(_UZERO) and rounds != _UZERO:
raise uh.exc.ZeroPaddedRoundsError(cls)
rounds = int(rounds)
implicit_rounds = False
else:
rounds = 5000
implicit_rounds = True
# rest should be salt and checksum
if len(parts) == 2:
salt, chk = parts
elif len(parts) == 1:
salt = parts[0]
chk = None
else:
raise uh.exc.MalformedHashError(cls)
# return new object
return cls(
rounds=rounds,
salt=salt,
checksum=chk or None,
implicit_rounds=implicit_rounds,
relaxed=not chk, # NOTE: relaxing parsing for config strings
# so that out-of-range rounds are clipped,
# since SHA2-Crypt spec treats them this way.
)
def to_string(self):
if self.rounds == 5000 and self.implicit_rounds:
hash = u("%s%s$%s") % (self.ident, self.salt,
self.checksum or u(''))
else:
hash = u("%srounds=%d$%s$%s") % (self.ident, self.rounds,
self.salt, self.checksum or u(''))
return uascii_to_str(hash)
#===================================================================
# backends
#===================================================================
backends = ("os_crypt", "builtin")
_has_backend_builtin = True
# _has_backend_os_crypt - provided by subclass
def _calc_checksum_builtin(self, secret):
return _raw_sha2_crypt(secret, self.salt, self.rounds,
self._cdb_use_512)
def _calc_checksum_os_crypt(self, secret):
hash = safe_crypt(secret, self.to_string())
if hash:
# NOTE: avoiding full parsing routine via from_string().checksum,
# and just extracting the bit we need.
cs = self.checksum_size
assert hash.startswith(self.ident) and hash[-cs-1] == _UDOLLAR
return hash[-cs:]
else:
return self._calc_checksum_builtin(secret)
#===================================================================
# eoc
#===================================================================
class sha256_crypt(_SHA2_Common):
"""This class implements the SHA256-Crypt password hash, and follows the :ref:`password-hash-api`.
It supports a variable-length salt, and a variable number of rounds.
The :meth:`~passlib.ifc.PasswordHash.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 0-16 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
:type rounds: int
:param rounds:
Optional number of rounds to use.
Defaults to 110000, must be between 1000 and 999999999, inclusive.
:type implicit_rounds: bool
:param implicit_rounds:
this is an internal option which generally doesn't need to be touched.
this flag determines whether the hash should omit the rounds parameter
when encoding it to a string; this is only permitted by the spec for rounds=5000,
and the flag is ignored otherwise. the spec requires the two different
encodings be preserved as they are, instead of normalizing them.
:type relaxed: bool
:param relaxed:
By default, providing an invalid value for one of the other
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
will be issued instead. Correctable errors include ``rounds``
that are too small or too large, and ``salt`` strings that are too long.
.. versionadded:: 1.6
"""
#===================================================================
# class attrs
#===================================================================
name = "sha256_crypt"
ident = u("$5$")
checksum_size = 43
# NOTE: using 25/75 weighting of builtin & os_crypt backends
default_rounds = 110000
#===================================================================
# backends
#===================================================================
@classproperty
def _has_backend_os_crypt(cls):
return test_crypt("test", "$5$rounds=1000$test$QmQADEXMG8POI5W"
"Dsaeho0P36yK3Tcrgboabng6bkb/")
#===================================================================
# eoc
#===================================================================
#=============================================================================
# sha 512 crypt
#=============================================================================
class sha512_crypt(_SHA2_Common):
"""This class implements the SHA512-Crypt password hash, and follows the :ref:`password-hash-api`.
It supports a variable-length salt, and a variable number of rounds.
The :meth:`~passlib.ifc.PasswordHash.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 0-16 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
:type rounds: int
:param rounds:
Optional number of rounds to use.
Defaults to 100000, must be between 1000 and 999999999, inclusive.
:type implicit_rounds: bool
:param implicit_rounds:
this is an internal option which generally doesn't need to be touched.
this flag determines whether the hash should omit the rounds parameter
when encoding it to a string; this is only permitted by the spec for rounds=5000,
and the flag is ignored otherwise. the spec requires the two different
encodings be preserved as they are, instead of normalizing them.
:type relaxed: bool
:param relaxed:
By default, providing an invalid value for one of the other
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
will be issued instead. Correctable errors include ``rounds``
that are too small or too large, and ``salt`` strings that are too long.
.. versionadded:: 1.6
"""
#===================================================================
# class attrs
#===================================================================
name = "sha512_crypt"
ident = u("$6$")
checksum_size = 86
_cdb_use_512 = True
# NOTE: using 25/75 weighting of builtin & os_crypt backends
default_rounds = 100000
#===================================================================
# backend
#===================================================================
@classproperty
def _has_backend_os_crypt(cls):
return test_crypt("test", "$6$rounds=1000$test$2M/Lx6Mtobqj"
"Ljobw0Wmo4Q5OFx5nVLJvmgseatA6oMn"
"yWeBdRDx4DU.1H3eGmse6pgsOgDisWBG"
"I5c7TZauS0")
#===================================================================
# eoc
#===================================================================
#=============================================================================
# eof
#=============================================================================

View File

@ -0,0 +1,364 @@
"""passlib.handlers.sun_md5_crypt - Sun's Md5 Crypt, used on Solaris
.. warning::
This implementation may not reproduce
the original Solaris behavior in some border cases.
See documentation for details.
"""
#=============================================================================
# imports
#=============================================================================
# core
from hashlib import md5
import re
import logging; log = logging.getLogger(__name__)
from warnings import warn
# site
# pkg
from passlib.utils import h64, to_unicode
from passlib.utils.compat import b, bytes, byte_elem_value, irange, u, \
uascii_to_str, unicode, str_to_bascii
import passlib.utils.handlers as uh
# local
__all__ = [
"sun_md5_crypt",
]
#=============================================================================
# backend
#=============================================================================
# constant data used by alg - Hamlet act 3 scene 1 + null char
# exact bytes as in http://www.ibiblio.org/pub/docs/books/gutenberg/etext98/2ws2610.txt
# from Project Gutenberg.
MAGIC_HAMLET = b(
"To be, or not to be,--that is the question:--\n"
"Whether 'tis nobler in the mind to suffer\n"
"The slings and arrows of outrageous fortune\n"
"Or to take arms against a sea of troubles,\n"
"And by opposing end them?--To die,--to sleep,--\n"
"No more; and by a sleep to say we end\n"
"The heartache, and the thousand natural shocks\n"
"That flesh is heir to,--'tis a consummation\n"
"Devoutly to be wish'd. To die,--to sleep;--\n"
"To sleep! perchance to dream:--ay, there's the rub;\n"
"For in that sleep of death what dreams may come,\n"
"When we have shuffled off this mortal coil,\n"
"Must give us pause: there's the respect\n"
"That makes calamity of so long life;\n"
"For who would bear the whips and scorns of time,\n"
"The oppressor's wrong, the proud man's contumely,\n"
"The pangs of despis'd love, the law's delay,\n"
"The insolence of office, and the spurns\n"
"That patient merit of the unworthy takes,\n"
"When he himself might his quietus make\n"
"With a bare bodkin? who would these fardels bear,\n"
"To grunt and sweat under a weary life,\n"
"But that the dread of something after death,--\n"
"The undiscover'd country, from whose bourn\n"
"No traveller returns,--puzzles the will,\n"
"And makes us rather bear those ills we have\n"
"Than fly to others that we know not of?\n"
"Thus conscience does make cowards of us all;\n"
"And thus the native hue of resolution\n"
"Is sicklied o'er with the pale cast of thought;\n"
"And enterprises of great pith and moment,\n"
"With this regard, their currents turn awry,\n"
"And lose the name of action.--Soft you now!\n"
"The fair Ophelia!--Nymph, in thy orisons\n"
"Be all my sins remember'd.\n\x00" #<- apparently null at end of C string is included (test vector won't pass otherwise)
)
# NOTE: these sequences are pre-calculated iteration ranges used by X & Y loops w/in rounds function below
xr = irange(7)
_XY_ROUNDS = [
tuple((i,i,i+3) for i in xr), # xrounds 0
tuple((i,i+1,i+4) for i in xr), # xrounds 1
tuple((i,i+8,(i+11)&15) for i in xr), # yrounds 0
tuple((i,(i+9)&15, (i+12)&15) for i in xr), # yrounds 1
]
del xr
def raw_sun_md5_crypt(secret, rounds, salt):
"given secret & salt, return encoded sun-md5-crypt checksum"
global MAGIC_HAMLET
assert isinstance(secret, bytes)
assert isinstance(salt, bytes)
# validate rounds
if rounds <= 0:
rounds = 0
real_rounds = 4096 + rounds
# NOTE: spec seems to imply max 'rounds' is 2**32-1
# generate initial digest to start off round 0.
# NOTE: algorithm 'salt' includes full config string w/ trailing "$"
result = md5(secret + salt).digest()
assert len(result) == 16
# NOTE: many things in this function have been inlined (to speed up the loop
# as much as possible), to the point that this code barely resembles
# the algorithm as described in the docs. in particular:
#
# * all accesses to a given bit have been inlined using the formula
# rbitval(bit) = (rval((bit>>3) & 15) >> (bit & 7)) & 1
#
# * the calculation of coinflip value R has been inlined
#
# * the conditional division of coinflip value V has been inlined as
# a shift right of 0 or 1.
#
# * the i, i+3, etc iterations are precalculated in lists.
#
# * the round-based conditional division of x & y is now performed
# by choosing an appropriate precalculated list, so that it only
# calculates the 7 bits which will actually be used.
#
X_ROUNDS_0, X_ROUNDS_1, Y_ROUNDS_0, Y_ROUNDS_1 = _XY_ROUNDS
# NOTE: % appears to be *slightly* slower than &, so we prefer & if possible
round = 0
while round < real_rounds:
# convert last result byte string to list of byte-ints for easy access
rval = [ byte_elem_value(c) for c in result ].__getitem__
# build up X bit by bit
x = 0
xrounds = X_ROUNDS_1 if (rval((round>>3) & 15)>>(round & 7)) & 1 else X_ROUNDS_0
for i, ia, ib in xrounds:
a = rval(ia)
b = rval(ib)
v = rval((a >> (b % 5)) & 15) >> ((b>>(a&7)) & 1)
x |= ((rval((v>>3)&15)>>(v&7))&1) << i
# build up Y bit by bit
y = 0
yrounds = Y_ROUNDS_1 if (rval(((round+64)>>3) & 15)>>(round & 7)) & 1 else Y_ROUNDS_0
for i, ia, ib in yrounds:
a = rval(ia)
b = rval(ib)
v = rval((a >> (b % 5)) & 15) >> ((b>>(a&7)) & 1)
y |= ((rval((v>>3)&15)>>(v&7))&1) << i
# extract x'th and y'th bit, xoring them together to yeild "coin flip"
coin = ((rval(x>>3) >> (x&7)) ^ (rval(y>>3) >> (y&7))) & 1
# construct hash for this round
h = md5(result)
if coin:
h.update(MAGIC_HAMLET)
h.update(unicode(round).encode("ascii"))
result = h.digest()
round += 1
# encode output
return h64.encode_transposed_bytes(result, _chk_offsets)
# NOTE: same offsets as md5_crypt
_chk_offsets = (
12,6,0,
13,7,1,
14,8,2,
15,9,3,
5,10,4,
11,
)
#=============================================================================
# handler
#=============================================================================
class sun_md5_crypt(uh.HasRounds, uh.HasSalt, uh.GenericHandler):
"""This class implements the Sun-MD5-Crypt password hash, and follows the :ref:`password-hash-api`.
It supports a variable-length salt, and a variable number of rounds.
The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
:type salt: str
:param salt:
Optional salt string.
If not specified, a salt will be autogenerated (this is recommended).
If specified, it must be drawn from the regexp range ``[./0-9A-Za-z]``.
:type salt_size: int
:param salt_size:
If no salt is specified, this parameter can be used to specify
the size (in characters) of the autogenerated salt.
It currently defaults to 8.
:type rounds: int
:param rounds:
Optional number of rounds to use.
Defaults to 5500, must be between 0 and 4294963199, inclusive.
:type bare_salt: bool
:param bare_salt:
Optional flag used to enable an alternate salt digest behavior
used by some hash strings in this scheme.
This flag can be ignored by most users.
Defaults to ``False``.
(see :ref:`smc-bare-salt` for details).
:type relaxed: bool
:param relaxed:
By default, providing an invalid value for one of the other
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
will be issued instead. Correctable errors include ``rounds``
that are too small or too large, and ``salt`` strings that are too long.
.. versionadded:: 1.6
"""
#===================================================================
# class attrs
#===================================================================
name = "sun_md5_crypt"
setting_kwds = ("salt", "rounds", "bare_salt", "salt_size")
checksum_chars = uh.HASH64_CHARS
checksum_size = 22
# NOTE: docs say max password length is 255.
# release 9u2
# NOTE: not sure if original crypt has a salt size limit,
# all instances that have been seen use 8 chars.
default_salt_size = 8
min_salt_size = 0
max_salt_size = None
salt_chars = uh.HASH64_CHARS
default_rounds = 5500 # current passlib default
min_rounds = 0
max_rounds = 4294963199 ##2**32-1-4096
# XXX: ^ not sure what it does if past this bound... does 32 int roll over?
rounds_cost = "linear"
ident_values = (u("$md5$"), u("$md5,"))
#===================================================================
# instance attrs
#===================================================================
bare_salt = False # flag to indicate legacy hashes that lack "$$" suffix
#===================================================================
# constructor
#===================================================================
def __init__(self, bare_salt=False, **kwds):
self.bare_salt = bare_salt
super(sun_md5_crypt, self).__init__(**kwds)
#===================================================================
# internal helpers
#===================================================================
@classmethod
def identify(cls, hash):
hash = uh.to_unicode_for_identify(hash)
return hash.startswith(cls.ident_values)
@classmethod
def from_string(cls, hash):
hash = to_unicode(hash, "ascii", "hash")
#
# detect if hash specifies rounds value.
# if so, parse and validate it.
# by end, set 'rounds' to int value, and 'tail' containing salt+chk
#
if hash.startswith(u("$md5$")):
rounds = 0
salt_idx = 5
elif hash.startswith(u("$md5,rounds=")):
idx = hash.find(u("$"), 12)
if idx == -1:
raise uh.exc.MalformedHashError(cls, "unexpected end of rounds")
rstr = hash[12:idx]
try:
rounds = int(rstr)
except ValueError:
raise uh.exc.MalformedHashError(cls, "bad rounds")
if rstr != unicode(rounds):
raise uh.exc.ZeroPaddedRoundsError(cls)
if rounds == 0:
# NOTE: not sure if this is forbidden by spec or not;
# but allowing it would complicate things,
# and it should never occur anyways.
raise uh.exc.MalformedHashError(cls, "explicit zero rounds")
salt_idx = idx+1
else:
raise uh.exc.InvalidHashError(cls)
#
# salt/checksum separation is kinda weird,
# to deal cleanly with some backward-compatible workarounds
# implemented by original implementation.
#
chk_idx = hash.rfind(u("$"), salt_idx)
if chk_idx == -1:
# ''-config for $-hash
salt = hash[salt_idx:]
chk = None
bare_salt = True
elif chk_idx == len(hash)-1:
if chk_idx > salt_idx and hash[-2] == u("$"):
raise uh.exc.MalformedHashError(cls, "too many '$' separators")
# $-config for $$-hash
salt = hash[salt_idx:-1]
chk = None
bare_salt = False
elif chk_idx > 0 and hash[chk_idx-1] == u("$"):
# $$-hash
salt = hash[salt_idx:chk_idx-1]
chk = hash[chk_idx+1:]
bare_salt = False
else:
# $-hash
salt = hash[salt_idx:chk_idx]
chk = hash[chk_idx+1:]
bare_salt = True
return cls(
rounds=rounds,
salt=salt,
checksum=chk,
bare_salt=bare_salt,
)
def to_string(self, withchk=True):
ss = u('') if self.bare_salt else u('$')
rounds = self.rounds
if rounds > 0:
hash = u("$md5,rounds=%d$%s%s") % (rounds, self.salt, ss)
else:
hash = u("$md5$%s%s") % (self.salt, ss)
if withchk:
chk = self.checksum
if chk:
hash = u("%s$%s") % (hash, chk)
return uascii_to_str(hash)
#===================================================================
# primary interface
#===================================================================
# TODO: if we're on solaris, check for native crypt() support.
# this will require extra testing, to make sure native crypt
# actually behaves correctly. of particular importance:
# when using ""-config, make sure to append "$x" to string.
def _calc_checksum(self, secret):
# NOTE: no reference for how sun_md5_crypt handles unicode
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
config = str_to_bascii(self.to_string(withchk=False))
return raw_sun_md5_crypt(secret, self.rounds, config).decode("ascii")
#===================================================================
# eoc
#===================================================================
#=============================================================================
# eof
#=============================================================================

310
passlib/handlers/windows.py Normal file
View File

@ -0,0 +1,310 @@
"""passlib.handlers.nthash - Microsoft Windows -related hashes"""
#=============================================================================
# imports
#=============================================================================
# core
from binascii import hexlify
import re
import logging; log = logging.getLogger(__name__)
from warnings import warn
# site
# pkg
from passlib.utils import to_unicode, right_pad_string
from passlib.utils.compat import b, bytes, str_to_uascii, u, unicode, uascii_to_str
from passlib.utils.md4 import md4
import passlib.utils.handlers as uh
# local
__all__ = [
"lmhash",
"nthash",
"bsd_nthash",
"msdcc",
"msdcc2",
]
#=============================================================================
# lanman hash
#=============================================================================
class lmhash(uh.HasEncodingContext, uh.StaticHandler):
"""This class implements the Lan Manager Password hash, and follows the :ref:`password-hash-api`.
It has no salt and a single fixed round.
The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.verify` methods accept a single
optional keyword:
:type encoding: str
:param encoding:
This specifies what character encoding LMHASH should use when
calculating digest. It defaults to ``cp437``, the most
common encoding encountered.
Note that while this class outputs digests in lower-case hexidecimal,
it will accept upper-case as well.
"""
#===================================================================
# class attrs
#===================================================================
name = "lmhash"
checksum_chars = uh.HEX_CHARS
checksum_size = 32
default_encoding = "cp437"
#===================================================================
# methods
#===================================================================
@classmethod
def _norm_hash(cls, hash):
return hash.lower()
def _calc_checksum(self, secret):
return hexlify(self.raw(secret, self.encoding)).decode("ascii")
# magic constant used by LMHASH
_magic = b("KGS!@#$%")
@classmethod
def raw(cls, secret, encoding=None):
"""encode password using LANMAN hash algorithm.
:type secret: unicode or utf-8 encoded bytes
:arg secret: secret to hash
:type encoding: str
:arg encoding:
optional encoding to use for unicode inputs.
this defaults to ``cp437``, which is the
common case for most situations.
:returns: returns string of raw bytes
"""
if not encoding:
encoding = cls.default_encoding
# some nice empircal data re: different encodings is at...
# http://www.openwall.com/lists/john-dev/2011/08/01/2
# http://www.freerainbowtables.com/phpBB3/viewtopic.php?t=387&p=12163
from passlib.utils.des import des_encrypt_block
MAGIC = cls._magic
if isinstance(secret, unicode):
# perform uppercasing while we're still unicode,
# to give a better shot at getting non-ascii chars right.
# (though some codepages do NOT upper-case the same as unicode).
secret = secret.upper().encode(encoding)
elif isinstance(secret, bytes):
# FIXME: just trusting ascii upper will work?
# and if not, how to do codepage specific case conversion?
# we could decode first using <encoding>,
# but *that* might not always be right.
secret = secret.upper()
else:
raise TypeError("secret must be unicode or bytes")
secret = right_pad_string(secret, 14)
return des_encrypt_block(secret[0:7], MAGIC) + \
des_encrypt_block(secret[7:14], MAGIC)
#===================================================================
# eoc
#===================================================================
#=============================================================================
# ntlm hash
#=============================================================================
class nthash(uh.StaticHandler):
"""This class implements the NT Password hash, and follows the :ref:`password-hash-api`.
It has no salt and a single fixed round.
The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept no optional keywords.
Note that while this class outputs lower-case hexidecimal digests,
it will accept upper-case digests as well.
"""
#===================================================================
# class attrs
#===================================================================
name = "nthash"
checksum_chars = uh.HEX_CHARS
checksum_size = 32
#===================================================================
# methods
#===================================================================
@classmethod
def _norm_hash(cls, hash):
return hash.lower()
def _calc_checksum(self, secret):
return hexlify(self.raw(secret)).decode("ascii")
@classmethod
def raw(cls, secret):
"""encode password using MD4-based NTHASH algorithm
:arg secret: secret as unicode or utf-8 encoded bytes
:returns: returns string of raw bytes
"""
secret = to_unicode(secret, "utf-8", param="secret")
# XXX: found refs that say only first 128 chars are used.
return md4(secret.encode("utf-16-le")).digest()
@classmethod
def raw_nthash(cls, secret, hex=False):
warn("nthash.raw_nthash() is deprecated, and will be removed "
"in Passlib 1.8, please use nthash.raw() instead",
DeprecationWarning)
ret = nthash.raw(secret)
return hexlify(ret).decode("ascii") if hex else ret
#===================================================================
# eoc
#===================================================================
bsd_nthash = uh.PrefixWrapper("bsd_nthash", nthash, prefix="$3$$", ident="$3$$",
doc="""The class support FreeBSD's representation of NTHASH
(which is compatible with the :ref:`modular-crypt-format`),
and follows the :ref:`password-hash-api`.
It has no salt and a single fixed round.
The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept no optional keywords.
""")
##class ntlm_pair(object):
## "combined lmhash & nthash"
## name = "ntlm_pair"
## setting_kwds = ()
## _hash_regex = re.compile(u"^(?P<lm>[0-9a-f]{32}):(?P<nt>[0-9][a-f]{32})$",
## re.I)
##
## @classmethod
## def identify(cls, hash):
## hash = to_unicode(hash, "latin-1", "hash")
## return len(hash) == 65 and cls._hash_regex.match(hash) is not None
##
## @classmethod
## def genconfig(cls):
## return None
##
## @classmethod
## def genhash(cls, secret, config):
## if config is not None and not cls.identify(config):
## raise uh.exc.InvalidHashError(cls)
## return cls.encrypt(secret)
##
## @classmethod
## def encrypt(cls, secret):
## return lmhash.encrypt(secret) + ":" + nthash.encrypt(secret)
##
## @classmethod
## def verify(cls, secret, hash):
## hash = to_unicode(hash, "ascii", "hash")
## m = cls._hash_regex.match(hash)
## if not m:
## raise uh.exc.InvalidHashError(cls)
## lm, nt = m.group("lm", "nt")
## # NOTE: verify against both in case encoding issue
## # causes one not to match.
## return lmhash.verify(secret, lm) or nthash.verify(secret, nt)
#=============================================================================
# msdcc v1
#=============================================================================
class msdcc(uh.HasUserContext, uh.StaticHandler):
"""This class implements Microsoft's Domain Cached Credentials password hash,
and follows the :ref:`password-hash-api`.
It has a fixed number of rounds, and uses the associated
username as the salt.
The :meth:`~passlib.ifc.PasswordHash.encrypt`, :meth:`~passlib.ifc.PasswordHash.genhash`, and :meth:`~passlib.ifc.PasswordHash.verify` methods
have the following optional keywords:
:type user: str
:param user:
String containing name of user account this password is associated with.
This is required to properly calculate the hash.
This keyword is case-insensitive, and should contain just the username
(e.g. ``Administrator``, not ``SOMEDOMAIN\\Administrator``).
Note that while this class outputs lower-case hexidecimal digests,
it will accept upper-case digests as well.
"""
name = "msdcc"
checksum_chars = uh.HEX_CHARS
checksum_size = 32
@classmethod
def _norm_hash(cls, hash):
return hash.lower()
def _calc_checksum(self, secret):
return hexlify(self.raw(secret, self.user)).decode("ascii")
@classmethod
def raw(cls, secret, user):
"""encode password using mscash v1 algorithm
:arg secret: secret as unicode or utf-8 encoded bytes
:arg user: username to use as salt
:returns: returns string of raw bytes
"""
secret = to_unicode(secret, "utf-8", param="secret").encode("utf-16-le")
user = to_unicode(user, "utf-8", param="user").lower().encode("utf-16-le")
return md4(md4(secret).digest() + user).digest()
#=============================================================================
# msdcc2 aka mscash2
#=============================================================================
class msdcc2(uh.HasUserContext, uh.StaticHandler):
"""This class implements version 2 of Microsoft's Domain Cached Credentials
password hash, and follows the :ref:`password-hash-api`.
It has a fixed number of rounds, and uses the associated
username as the salt.
The :meth:`~passlib.ifc.PasswordHash.encrypt`, :meth:`~passlib.ifc.PasswordHash.genhash`, and :meth:`~passlib.ifc.PasswordHash.verify` methods
have the following extra keyword:
:type user: str
:param user:
String containing name of user account this password is associated with.
This is required to properly calculate the hash.
This keyword is case-insensitive, and should contain just the username
(e.g. ``Administrator``, not ``SOMEDOMAIN\\Administrator``).
"""
name = "msdcc2"
checksum_chars = uh.HEX_CHARS
checksum_size = 32
@classmethod
def _norm_hash(cls, hash):
return hash.lower()
def _calc_checksum(self, secret):
return hexlify(self.raw(secret, self.user)).decode("ascii")
@classmethod
def raw(cls, secret, user):
"""encode password using msdcc v2 algorithm
:type secret: unicode or utf-8 bytes
:arg secret: secret
:type user: str
:arg user: username to use as salt
:returns: returns string of raw bytes
"""
from passlib.utils.pbkdf2 import pbkdf2
secret = to_unicode(secret, "utf-8", param="secret").encode("utf-16-le")
user = to_unicode(user, "utf-8", param="user").lower().encode("utf-16-le")
tmp = md4(md4(secret).digest() + user).digest()
return pbkdf2(tmp, user, 10240, 16, 'hmac-sha1')
#=============================================================================
# eof
#=============================================================================

28
passlib/hash.py Normal file
View File

@ -0,0 +1,28 @@
"""passlib.hash - proxy object mapping hash scheme names -> handlers
Note
====
This module does not actually contain any hashes. This file
is a stub that replaces itself with a proxy object.
This proxy object (passlib.registry._PasslibRegistryProxy)
handles lazy-loading hashes as they are requested.
The actual implementation of the various hashes is store elsewhere,
mainly in the submodules of the ``passlib.handlers`` package.
"""
# NOTE: could support 'non-lazy' version which just imports
# all schemes known to list_crypt_handlers()
#=============================================================================
# import proxy object and replace this module
#=============================================================================
from passlib.registry import _proxy
import sys
sys.modules[__name__] = _proxy
#=============================================================================
# eoc
#=============================================================================

115
passlib/hosts.py Normal file
View File

@ -0,0 +1,115 @@
"""passlib.hosts"""
#=============================================================================
# imports
#=============================================================================
# core
import sys
from warnings import warn
# pkg
from passlib.context import LazyCryptContext
from passlib.exc import PasslibRuntimeWarning
from passlib.registry import get_crypt_handler
from passlib.utils import has_crypt, unix_crypt_schemes
# local
__all__ = [
"linux_context", "linux2_context",
"openbsd_context",
"netbsd_context",
"freebsd_context",
"host_context",
]
#=============================================================================
# linux support
#=============================================================================
# known platform names - linux2
linux_context = linux2_context = LazyCryptContext(
schemes = [ "sha512_crypt", "sha256_crypt", "md5_crypt",
"des_crypt", "unix_disabled" ],
deprecated = [ "des_crypt" ],
)
#=============================================================================
# bsd support
#=============================================================================
# known platform names -
# freebsd2
# freebsd3
# freebsd4
# freebsd5
# freebsd6
# freebsd7
#
# netbsd1
# referencing source via -http://fxr.googlebit.com
# freebsd 6,7,8 - des, md5, bcrypt, bsd_nthash
# netbsd - des, ext, md5, bcrypt, sha1
# openbsd - des, ext, md5, bcrypt
freebsd_context = LazyCryptContext(["bcrypt", "md5_crypt", "bsd_nthash",
"des_crypt", "unix_disabled"])
openbsd_context = LazyCryptContext(["bcrypt", "md5_crypt", "bsdi_crypt",
"des_crypt", "unix_disabled"])
netbsd_context = LazyCryptContext(["bcrypt", "sha1_crypt", "md5_crypt",
"bsdi_crypt", "des_crypt", "unix_disabled"])
# XXX: include darwin in this list? it's got a BSD crypt variant,
# but that's not what it uses for user passwords.
#=============================================================================
# current host
#=============================================================================
if has_crypt:
# NOTE: this is basically mimicing the output of os crypt(),
# except that it uses passlib's (usually stronger) defaults settings,
# and can be introspected and used much more flexibly.
def _iter_os_crypt_schemes():
"helper which iterates over supported os_crypt schemes"
found = False
for name in unix_crypt_schemes:
handler = get_crypt_handler(name)
if handler.has_backend("os_crypt"):
found = True
yield name
if found:
# only offer disabled handler if there's another scheme in front,
# as this can't actually hash any passwords
yield "unix_disabled"
else: # pragma: no cover -- sanity check
# no idea what OS this could happen on...
warn("crypt.crypt() function is present, but doesn't support any "
"formats known to passlib!", PasslibRuntimeWarning)
host_context = LazyCryptContext(_iter_os_crypt_schemes())
#=============================================================================
# other platforms
#=============================================================================
# known platform strings -
# aix3
# aix4
# atheos
# beos5
# darwin
# generic
# hp-ux11
# irix5
# irix6
# mac
# next3
# os2emx
# riscos
# sunos5
# unixware7
#=============================================================================
# eof
#=============================================================================

193
passlib/ifc.py Normal file
View File

@ -0,0 +1,193 @@
"""passlib.ifc - abstract interfaces used by Passlib"""
#=============================================================================
# imports
#=============================================================================
# core
import logging; log = logging.getLogger(__name__)
import sys
# site
# pkg
# local
__all__ = [
"PasswordHash",
]
#=============================================================================
# 2.5-3.2 compatibility helpers
#=============================================================================
if sys.version_info >= (2,6):
from abc import ABCMeta, abstractmethod, abstractproperty
else:
# create stub for python 2.5
ABCMeta = type
def abstractmethod(func):
return func
# def abstractproperty():
# return None
def create_with_metaclass(meta):
"class decorator that re-creates class using metaclass"
# have to do things this way since abc not present in py25,
# and py2/py3 have different ways of doing metaclasses.
def builder(cls):
if meta is type(cls):
return cls
return meta(cls.__name__, cls.__bases__, cls.__dict__.copy())
return builder
#=============================================================================
# PasswordHash interface
#=============================================================================
class PasswordHash(object):
"""This class describes an abstract interface which all password hashes
in Passlib adhere to. Under Python 2.6 and up, this is an actual
Abstract Base Class built using the :mod:`!abc` module.
See the Passlib docs for full documentation.
"""
#===================================================================
# class attributes
#===================================================================
#---------------------------------------------------------------
# general information
#---------------------------------------------------------------
##name
##setting_kwds
##context_kwds
#---------------------------------------------------------------
# salt information -- if 'salt' in setting_kwds
#---------------------------------------------------------------
##min_salt_size
##max_salt_size
##default_salt_size
##salt_chars
##default_salt_chars
#---------------------------------------------------------------
# rounds information -- if 'rounds' in setting_kwds
#---------------------------------------------------------------
##min_rounds
##max_rounds
##default_rounds
##rounds_cost
#---------------------------------------------------------------
# encoding info -- if 'encoding' in context_kwds
#---------------------------------------------------------------
##default_encoding
#===================================================================
# primary methods
#===================================================================
@classmethod
@abstractmethod
def encrypt(cls, secret, **setting_and_context_kwds): # pragma: no cover -- abstract method
"encrypt secret, returning resulting hash"
raise NotImplementedError("must be implemented by subclass")
@classmethod
@abstractmethod
def verify(cls, secret, hash, **context_kwds): # pragma: no cover -- abstract method
"verify secret against hash, returns True/False"
raise NotImplementedError("must be implemented by subclass")
#===================================================================
# additional methods
#===================================================================
@classmethod
@abstractmethod
def identify(cls, hash): # pragma: no cover -- abstract method
"check if hash belongs to this scheme, returns True/False"
raise NotImplementedError("must be implemented by subclass")
@classmethod
@abstractmethod
def genconfig(cls, **setting_kwds): # pragma: no cover -- abstract method
"compile settings into a configuration string for genhash()"
raise NotImplementedError("must be implemented by subclass")
@classmethod
@abstractmethod
def genhash(cls, secret, config, **context_kwds): # pragma: no cover -- abstract method
"generated hash for secret, using settings from config/hash string"
raise NotImplementedError("must be implemented by subclass")
#===================================================================
# undocumented methods / attributes
#===================================================================
# the following entry points are used internally by passlib,
# and aren't documented as part of the exposed interface.
# they are subject to change between releases,
# but are documented here so there's a list of them *somewhere*.
#---------------------------------------------------------------
# checksum information - defined for many hashes
#---------------------------------------------------------------
## checksum_chars
## checksum_size
#---------------------------------------------------------------
# CryptContext flags
#---------------------------------------------------------------
# hack for bsdi_crypt: if True, causes CryptContext to only generate
# odd rounds values. assumed False if not defined.
## _avoid_even_rounds = False
##@classmethod
##def _bind_needs_update(cls, **setting_kwds):
## """return helper to detect hashes that need updating.
##
## if this method is defined, the CryptContext constructor
## will invoke it with the settings specified for the context.
## this method should return either ``None``, or a callable
## with the signature ``needs_update(hash,secret)->bool``.
##
## this ``needs_update`` function should return True if the hash
## should be re-encrypted, whether due to internal
## issues or the specified settings.
##
## CryptContext will automatically take care of deprecating
## hashes with insufficient rounds for classes which define fromstring()
## and a rounds attribute - though the requirements for this last
## part may change at some point.
## """
#---------------------------------------------------------------
# experimental methods
#---------------------------------------------------------------
##@classmethod
##def normhash(cls, hash):
## """helper to clean up non-canonic instances of hash.
## currently only provided by bcrypt() to fix an historical passlib issue.
## """
# experimental helper to parse hash into components.
##@classmethod
##def parsehash(cls, hash, checksum=True, sanitize=False):
## """helper to parse hash into components, returns dict"""
# experiment helper to estimate bitsize of different hashes,
# implement for GenericHandler, but may be currently be off for some hashes.
# want to expand this into a way to programmatically compare
# "strengths" of different hashes and hash algorithms.
# still needs to have some factor for estimate relative cost per round,
# ala in the style of the scrypt whitepaper.
##@classmethod
##def bitsize(cls, **kwds):
## """returns dict mapping component -> bits contributed.
## components currently include checksum, salt, rounds.
## """
#===================================================================
# eoc
#===================================================================
PasswordHash = create_with_metaclass(ABCMeta)(PasswordHash)
#=============================================================================
# eof
#=============================================================================

411
passlib/registry.py Normal file
View File

@ -0,0 +1,411 @@
"""passlib.registry - registry for password hash handlers"""
#=============================================================================
# imports
#=============================================================================
# core
import re
import logging; log = logging.getLogger(__name__)
from warnings import warn
# pkg
from passlib.exc import ExpectedTypeError, PasslibWarning
from passlib.utils import is_crypt_handler
# local
__all__ = [
"register_crypt_handler_path",
"register_crypt_handler",
"get_crypt_handler",
"list_crypt_handlers",
]
#=============================================================================
# proxy object used in place of 'passlib.hash' module
#=============================================================================
class _PasslibRegistryProxy(object):
"""proxy module passlib.hash
this module is in fact an object which lazy-loads
the requested password hash algorithm from wherever it has been stored.
it acts as a thin wrapper around :func:`passlib.registry.get_crypt_handler`.
"""
__name__ = "passlib.hash"
__package__ = None
def __getattr__(self, attr):
if attr.startswith("_"):
raise AttributeError("missing attribute: %r" % (attr,))
handler = get_crypt_handler(attr, None)
if handler:
return handler
else:
raise AttributeError("unknown password hash: %r" % (attr,))
def __setattr__(self, attr, value):
if attr.startswith("_"):
# writing to private attributes should behave normally.
# (required so GAE can write to the __loader__ attribute).
object.__setattr__(self, attr, value)
else:
# writing to public attributes should be treated
# as attempting to register a handler.
register_crypt_handler(value, _attr=attr)
def __repr__(self):
return "<proxy module 'passlib.hash'>"
def __dir__(self):
# this adds in lazy-loaded handler names,
# otherwise this is the standard dir() implementation.
attrs = set(dir(self.__class__))
attrs.update(self.__dict__)
attrs.update(_locations)
return sorted(attrs)
# create single instance - available publically as 'passlib.hash'
_proxy = _PasslibRegistryProxy()
#=============================================================================
# internal registry state
#=============================================================================
# singleton uses to detect omitted keywords
_UNSET = object()
# dict mapping name -> loaded handlers (just uses proxy object's internal dict)
_handlers = _proxy.__dict__
# dict mapping names -> import path for lazy loading.
# * import path should be "module.path" or "module.path:attr"
# * if attr omitted, "name" used as default.
_locations = dict(
# NOTE: this is a hardcoded list of the handlers built into passlib,
# applications should call register_crypt_handler_path()
apr_md5_crypt = "passlib.handlers.md5_crypt",
atlassian_pbkdf2_sha1 = "passlib.handlers.pbkdf2",
bcrypt = "passlib.handlers.bcrypt",
bcrypt_sha256 = "passlib.handlers.bcrypt",
bigcrypt = "passlib.handlers.des_crypt",
bsd_nthash = "passlib.handlers.windows",
bsdi_crypt = "passlib.handlers.des_crypt",
cisco_pix = "passlib.handlers.cisco",
cisco_type7 = "passlib.handlers.cisco",
cta_pbkdf2_sha1 = "passlib.handlers.pbkdf2",
crypt16 = "passlib.handlers.des_crypt",
des_crypt = "passlib.handlers.des_crypt",
django_bcrypt = "passlib.handlers.django",
django_bcrypt_sha256 = "passlib.handlers.django",
django_pbkdf2_sha256 = "passlib.handlers.django",
django_pbkdf2_sha1 = "passlib.handlers.django",
django_salted_sha1 = "passlib.handlers.django",
django_salted_md5 = "passlib.handlers.django",
django_des_crypt = "passlib.handlers.django",
django_disabled = "passlib.handlers.django",
dlitz_pbkdf2_sha1 = "passlib.handlers.pbkdf2",
fshp = "passlib.handlers.fshp",
grub_pbkdf2_sha512 = "passlib.handlers.pbkdf2",
hex_md4 = "passlib.handlers.digests",
hex_md5 = "passlib.handlers.digests",
hex_sha1 = "passlib.handlers.digests",
hex_sha256 = "passlib.handlers.digests",
hex_sha512 = "passlib.handlers.digests",
htdigest = "passlib.handlers.digests",
ldap_plaintext = "passlib.handlers.ldap_digests",
ldap_md5 = "passlib.handlers.ldap_digests",
ldap_sha1 = "passlib.handlers.ldap_digests",
ldap_hex_md5 = "passlib.handlers.roundup",
ldap_hex_sha1 = "passlib.handlers.roundup",
ldap_salted_md5 = "passlib.handlers.ldap_digests",
ldap_salted_sha1 = "passlib.handlers.ldap_digests",
ldap_des_crypt = "passlib.handlers.ldap_digests",
ldap_bsdi_crypt = "passlib.handlers.ldap_digests",
ldap_md5_crypt = "passlib.handlers.ldap_digests",
ldap_bcrypt = "passlib.handlers.ldap_digests",
ldap_sha1_crypt = "passlib.handlers.ldap_digests",
ldap_sha256_crypt = "passlib.handlers.ldap_digests",
ldap_sha512_crypt = "passlib.handlers.ldap_digests",
ldap_pbkdf2_sha1 = "passlib.handlers.pbkdf2",
ldap_pbkdf2_sha256 = "passlib.handlers.pbkdf2",
ldap_pbkdf2_sha512 = "passlib.handlers.pbkdf2",
lmhash = "passlib.handlers.windows",
md5_crypt = "passlib.handlers.md5_crypt",
msdcc = "passlib.handlers.windows",
msdcc2 = "passlib.handlers.windows",
mssql2000 = "passlib.handlers.mssql",
mssql2005 = "passlib.handlers.mssql",
mysql323 = "passlib.handlers.mysql",
mysql41 = "passlib.handlers.mysql",
nthash = "passlib.handlers.windows",
oracle10 = "passlib.handlers.oracle",
oracle11 = "passlib.handlers.oracle",
pbkdf2_sha1 = "passlib.handlers.pbkdf2",
pbkdf2_sha256 = "passlib.handlers.pbkdf2",
pbkdf2_sha512 = "passlib.handlers.pbkdf2",
phpass = "passlib.handlers.phpass",
plaintext = "passlib.handlers.misc",
postgres_md5 = "passlib.handlers.postgres",
roundup_plaintext = "passlib.handlers.roundup",
scram = "passlib.handlers.scram",
sha1_crypt = "passlib.handlers.sha1_crypt",
sha256_crypt = "passlib.handlers.sha2_crypt",
sha512_crypt = "passlib.handlers.sha2_crypt",
sun_md5_crypt = "passlib.handlers.sun_md5_crypt",
unix_disabled = "passlib.handlers.misc",
unix_fallback = "passlib.handlers.misc",
)
# master regexp for detecting valid handler names
_name_re = re.compile("^[a-z][a-z0-9_]+[a-z0-9]$")
# names which aren't allowed for various reasons
# (mainly keyword conflicts in CryptContext)
_forbidden_names = frozenset(["onload", "policy", "context", "all",
"default", "none", "auto"])
#=============================================================================
# registry frontend functions
#=============================================================================
def _validate_handler_name(name):
"""helper to validate handler name
:raises ValueError:
* if empty name
* if name not lower case
* if name contains double underscores
* if name is reserved (e.g. ``context``, ``all``).
"""
if not name:
raise ValueError("handler name cannot be empty: %r" % (name,))
if name.lower() != name:
raise ValueError("name must be lower-case: %r" % (name,))
if not _name_re.match(name):
raise ValueError("invalid name (must be 3+ characters, "
" begin with a-z, and contain only underscore, a-z, "
"0-9): %r" % (name,))
if '__' in name:
raise ValueError("name may not contain double-underscores: %r" %
(name,))
if name in _forbidden_names:
raise ValueError("that name is not allowed: %r" % (name,))
return True
def register_crypt_handler_path(name, path):
"""register location to lazy-load handler when requested.
custom hashes may be registered via :func:`register_crypt_handler`,
or they may be registered by this function,
which will delay actually importing and loading the handler
until a call to :func:`get_crypt_handler` is made for the specified name.
:arg name: name of handler
:arg path: module import path
the specified module path should contain a password hash handler
called :samp:`{name}`, or the path may contain a colon,
specifying the module and module attribute to use.
for example, the following would cause ``get_handler("myhash")`` to look
for a class named ``myhash`` within the ``myapp.helpers`` module::
>>> from passlib.registry import registry_crypt_handler_path
>>> registry_crypt_handler_path("myhash", "myapp.helpers")
...while this form would cause ``get_handler("myhash")`` to look
for a class name ``MyHash`` within the ``myapp.helpers`` module::
>>> from passlib.registry import registry_crypt_handler_path
>>> registry_crypt_handler_path("myhash", "myapp.helpers:MyHash")
"""
# validate name
_validate_handler_name(name)
# validate path
if path.startswith("."):
raise ValueError("path cannot start with '.'")
if ':' in path:
if path.count(':') > 1:
raise ValueError("path cannot have more than one ':'")
if path.find('.', path.index(':')) > -1:
raise ValueError("path cannot have '.' to right of ':'")
# store location
_locations[name] = path
log.debug("registered path to %r handler: %r", name, path)
def register_crypt_handler(handler, force=False, _attr=None):
"""register password hash handler.
this method immediately registers a handler with the internal passlib registry,
so that it will be returned by :func:`get_crypt_handler` when requested.
:arg handler: the password hash handler to register
:param force: force override of existing handler (defaults to False)
:param _attr:
[internal kwd] if specified, ensures ``handler.name``
matches this value, or raises :exc:`ValueError`.
:raises TypeError:
if the specified object does not appear to be a valid handler.
:raises ValueError:
if the specified object's name (or other required attributes)
contain invalid values.
:raises KeyError:
if a (different) handler was already registered with
the same name, and ``force=True`` was not specified.
"""
# validate handler
if not is_crypt_handler(handler):
raise ExpectedTypeError(handler, "password hash handler", "handler")
if not handler:
raise AssertionError("``bool(handler)`` must be True")
# validate name
name = handler.name
_validate_handler_name(name)
if _attr and _attr != name:
raise ValueError("handlers must be stored only under their own name")
# check for existing handler
other = _handlers.get(name)
if other:
if other is handler:
log.debug("same %r handler already registered: %r", name, handler)
return
elif force:
log.warning("overriding previously registered %r handler: %r",
name, other)
else:
raise KeyError("another %r handler has already been registered: %r" %
(name, other))
# register handler
_handlers[name] = handler
log.debug("registered %r handler: %r", name, handler)
def get_crypt_handler(name, default=_UNSET):
"""return handler for specified password hash scheme.
this method looks up a handler for the specified scheme.
if the handler is not already loaded,
it checks if the location is known, and loads it first.
:arg name: name of handler to return
:param default: optional default value to return if no handler with specified name is found.
:raises KeyError: if no handler matching that name is found, and no default specified, a KeyError will be raised.
:returns: handler attached to name, or default value (if specified).
"""
# catch invalid names before we check _handlers,
# since it's a module dict, and exposes things like __package__, etc.
if name.startswith("_"):
if default is _UNSET:
raise KeyError("invalid handler name: %r" % (name,))
else:
return default
# check if handler is already loaded
try:
return _handlers[name]
except KeyError:
pass
# normalize name (and if changed, check dict again)
assert isinstance(name, str), "name must be str instance"
alt = name.replace("-","_").lower()
if alt != name:
warn("handler names should be lower-case, and use underscores instead "
"of hyphens: %r => %r" % (name, alt), PasslibWarning,
stacklevel=2)
name = alt
# try to load using new name
try:
return _handlers[name]
except KeyError:
pass
# check if lazy load mapping has been specified for this driver
path = _locations.get(name)
if path:
if ':' in path:
modname, modattr = path.split(":")
else:
modname, modattr = path, name
##log.debug("loading %r handler from path: '%s:%s'", name, modname, modattr)
# try to load the module - any import errors indicate runtime config, usually
# either missing package, or bad path provided to register_crypt_handler_path()
mod = __import__(modname, fromlist=[modattr], level=0)
# first check if importing module triggered register_crypt_handler(),
# (this is discouraged due to it's magical implicitness)
handler = _handlers.get(name)
if handler:
# XXX: issue deprecation warning here?
assert is_crypt_handler(handler), "unexpected object: name=%r object=%r" % (name, handler)
return handler
# then get real handler & register it
handler = getattr(mod, modattr)
register_crypt_handler(handler, _attr=name)
return handler
# fail!
if default is _UNSET:
raise KeyError("no crypt handler found for algorithm: %r" % (name,))
else:
return default
def list_crypt_handlers(loaded_only=False):
"""return sorted list of all known crypt handler names.
:param loaded_only: if ``True``, only returns names of handlers which have actually been loaded.
:returns: list of names of all known handlers
"""
names = set(_handlers)
if not loaded_only:
names.update(_locations)
# strip private attrs out of namespace and sort.
# TODO: make _handlers a separate list, so we don't have module namespace mixed in.
return sorted(name for name in names if not name.startswith("_"))
# NOTE: these two functions mainly exist just for the unittests...
def _has_crypt_handler(name, loaded_only=False):
"""check if handler name is known.
this is only useful for two cases:
* quickly checking if handler has already been loaded
* checking if handler exists, without actually loading it
:arg name: name of handler
:param loaded_only: if ``True``, returns False if handler exists but hasn't been loaded
"""
return (name in _handlers) or (not loaded_only and name in _locations)
def _unload_handler_name(name, locations=True):
"""unloads a handler from the registry.
.. warning::
this is an internal function,
used only by the unittests.
if loaded handler is found with specified name, it's removed.
if path to lazy load handler is found, its' removed.
missing names are a noop.
:arg name: name of handler to unload
:param locations: if False, won't purge registered handler locations (default True)
"""
if name in _handlers:
del _handlers[name]
if locations and name in _locations:
del _locations[name]
#=============================================================================
# eof
#=============================================================================

View File

@ -0,0 +1 @@
"""passlib tests"""

View File

@ -0,0 +1,6 @@
import os
from nose import run
run(
defaultTest=os.path.dirname(__file__),
)

View File

@ -0,0 +1,15 @@
"helper for method in test_registry.py"
from passlib.registry import register_crypt_handler
import passlib.utils.handlers as uh
class dummy_bad(uh.StaticHandler):
name = "dummy_bad"
class alt_dummy_bad(uh.StaticHandler):
name = "dummy_bad"
# NOTE: if passlib.tests is being run from symlink (e.g. via gaeunit),
# this module may be imported a second time as test._test_bad_registry.
# we don't want it to do anything in that case.
if __name__.startswith("passlib.tests"):
register_crypt_handler(alt_dummy_bad)

329
passlib/tests/backports.py Normal file
View File

@ -0,0 +1,329 @@
"""backports of needed unittest2 features"""
#=============================================================================
# imports
#=============================================================================
from __future__ import with_statement
# core
import logging; log = logging.getLogger(__name__)
import re
import sys
##from warnings import warn
# site
# pkg
from passlib.utils.compat import base_string_types
# local
__all__ = [
"TestCase",
"skip", "skipIf", "skipUnless"
"catch_warnings",
]
#=============================================================================
# import latest unittest module available
#=============================================================================
try:
import unittest2 as unittest
ut_version = 2
except ImportError:
import unittest
if sys.version_info < (2,7) or (3,0) <= sys.version_info < (3,2):
# older versions of python will need to install the unittest2
# backport (named unittest2_3k for 3.0/3.1)
##warn("please install unittest2 for python %d.%d, it will be required "
## "as of passlib 1.x" % sys.version_info[:2])
ut_version = 1
else:
ut_version = 2
#=============================================================================
# backport SkipTest support using nose
#=============================================================================
if ut_version < 2:
# used to provide replacement SkipTest() error
from nose.plugins.skip import SkipTest
# hack up something to simulate skip() decorator
import functools
def skip(reason):
def decorator(test_item):
if isinstance(test_item, type) and issubclass(test_item, unittest.TestCase):
class skip_wrapper(test_item):
def setUp(self):
raise SkipTest(reason)
else:
@functools.wraps(test_item)
def skip_wrapper(*args, **kwargs):
raise SkipTest(reason)
return skip_wrapper
return decorator
def skipIf(condition, reason):
if condition:
return skip(reason)
else:
return lambda item: item
def skipUnless(condition, reason):
if condition:
return lambda item: item
else:
return skip(reason)
else:
skip = unittest.skip
skipIf = unittest.skipIf
skipUnless = unittest.skipUnless
#=============================================================================
# custom test harness
#=============================================================================
class TestCase(unittest.TestCase):
"""backports a number of unittest2 features in TestCase"""
#===================================================================
# backport some methods from unittest2
#===================================================================
if ut_version < 2:
#----------------------------------------------------------------
# simplistic backport of addCleanup() framework
#----------------------------------------------------------------
_cleanups = None
def addCleanup(self, function, *args, **kwds):
queue = self._cleanups
if queue is None:
queue = self._cleanups = []
queue.append((function, args, kwds))
def doCleanups(self):
queue = self._cleanups
while queue:
func, args, kwds = queue.pop()
func(*args, **kwds)
def tearDown(self):
self.doCleanups()
unittest.TestCase.tearDown(self)
#----------------------------------------------------------------
# backport skipTest (requires nose to work)
#----------------------------------------------------------------
def skipTest(self, reason):
raise SkipTest(reason)
#----------------------------------------------------------------
# backport various assert tests added in unittest2
#----------------------------------------------------------------
def assertIs(self, real, correct, msg=None):
if real is not correct:
std = "got %r, expected would be %r" % (real, correct)
msg = self._formatMessage(msg, std)
raise self.failureException(msg)
def assertIsNot(self, real, correct, msg=None):
if real is correct:
std = "got %r, expected would not be %r" % (real, correct)
msg = self._formatMessage(msg, std)
raise self.failureException(msg)
def assertIsInstance(self, obj, klass, msg=None):
if not isinstance(obj, klass):
std = "got %r, expected instance of %r" % (obj, klass)
msg = self._formatMessage(msg, std)
raise self.failureException(msg)
def assertAlmostEqual(self, first, second, places=None, msg=None, delta=None):
"""Fail if the two objects are unequal as determined by their
difference rounded to the given number of decimal places
(default 7) and comparing to zero, or by comparing that the
between the two objects is more than the given delta.
Note that decimal places (from zero) are usually not the same
as significant digits (measured from the most signficant digit).
If the two objects compare equal then they will automatically
compare almost equal.
"""
if first == second:
# shortcut
return
if delta is not None and places is not None:
raise TypeError("specify delta or places not both")
if delta is not None:
if abs(first - second) <= delta:
return
standardMsg = '%s != %s within %s delta' % (repr(first),
repr(second),
repr(delta))
else:
if places is None:
places = 7
if round(abs(second-first), places) == 0:
return
standardMsg = '%s != %s within %r places' % (repr(first),
repr(second),
places)
msg = self._formatMessage(msg, standardMsg)
raise self.failureException(msg)
def assertLess(self, left, right, msg=None):
if left >= right:
std = "%r not less than %r" % (left, right)
raise self.failureException(self._formatMessage(msg, std))
def assertGreater(self, left, right, msg=None):
if left <= right:
std = "%r not greater than %r" % (left, right)
raise self.failureException(self._formatMessage(msg, std))
def assertGreaterEqual(self, left, right, msg=None):
if left < right:
std = "%r less than %r" % (left, right)
raise self.failureException(self._formatMessage(msg, std))
def assertIn(self, elem, container, msg=None):
if elem not in container:
std = "%r not found in %r" % (elem, container)
raise self.failureException(self._formatMessage(msg, std))
def assertNotIn(self, elem, container, msg=None):
if elem in container:
std = "%r unexpectedly in %r" % (elem, container)
raise self.failureException(self._formatMessage(msg, std))
#----------------------------------------------------------------
# override some unittest1 methods to support _formatMessage
#----------------------------------------------------------------
def assertEqual(self, real, correct, msg=None):
if real != correct:
std = "got %r, expected would equal %r" % (real, correct)
msg = self._formatMessage(msg, std)
raise self.failureException(msg)
def assertNotEqual(self, real, correct, msg=None):
if real == correct:
std = "got %r, expected would not equal %r" % (real, correct)
msg = self._formatMessage(msg, std)
raise self.failureException(msg)
#---------------------------------------------------------------
# backport assertRegex() alias from 3.2 to 2.7/3.1
#---------------------------------------------------------------
if not hasattr(unittest.TestCase, "assertRegex"):
if hasattr(unittest.TestCase, "assertRegexpMatches"):
# was present in 2.7/3.1 under name assertRegexpMatches
assertRegex = unittest.TestCase.assertRegexpMatches
else:
# 3.0 and <= 2.6 didn't have this method at all
def assertRegex(self, text, expected_regex, msg=None):
"""Fail the test unless the text matches the regular expression."""
if isinstance(expected_regex, base_string_types):
assert expected_regex, "expected_regex must not be empty."
expected_regex = re.compile(expected_regex)
if not expected_regex.search(text):
msg = msg or "Regex didn't match: "
std = '%r not found in %r' % (msg, expected_regex.pattern, text)
raise self.failureException(self._formatMessage(msg, std))
#===================================================================
# eoc
#===================================================================
#=============================================================================
# backport catch_warnings
#=============================================================================
try:
from warnings import catch_warnings
except ImportError:
# catch_warnings wasn't added until py26.
# this adds backported copy from py26's stdlib
# so we can use it under py25.
class WarningMessage(object):
"""Holds the result of a single showwarning() call."""
_WARNING_DETAILS = ("message", "category", "filename", "lineno", "file",
"line")
def __init__(self, message, category, filename, lineno, file=None,
line=None):
local_values = locals()
for attr in self._WARNING_DETAILS:
setattr(self, attr, local_values[attr])
self._category_name = category.__name__ if category else None
def __str__(self):
return ("{message : %r, category : %r, filename : %r, lineno : %s, "
"line : %r}" % (self.message, self._category_name,
self.filename, self.lineno, self.line))
class catch_warnings(object):
"""A context manager that copies and restores the warnings filter upon
exiting the context.
The 'record' argument specifies whether warnings should be captured by a
custom implementation of warnings.showwarning() and be appended to a list
returned by the context manager. Otherwise None is returned by the context
manager. The objects appended to the list are arguments whose attributes
mirror the arguments to showwarning().
The 'module' argument is to specify an alternative module to the module
named 'warnings' and imported under that name. This argument is only useful
when testing the warnings module itself.
"""
def __init__(self, record=False, module=None):
"""Specify whether to record warnings and if an alternative module
should be used other than sys.modules['warnings'].
For compatibility with Python 3.0, please consider all arguments to be
keyword-only.
"""
self._record = record
self._module = sys.modules['warnings'] if module is None else module
self._entered = False
def __repr__(self):
args = []
if self._record:
args.append("record=True")
if self._module is not sys.modules['warnings']:
args.append("module=%r" % self._module)
name = type(self).__name__
return "%s(%s)" % (name, ", ".join(args))
def __enter__(self):
if self._entered:
raise RuntimeError("Cannot enter %r twice" % self)
self._entered = True
self._filters = self._module.filters
self._module.filters = self._filters[:]
self._showwarning = self._module.showwarning
if self._record:
log = []
def showwarning(*args, **kwargs):
# self._showwarning(*args, **kwargs)
log.append(WarningMessage(*args, **kwargs))
self._module.showwarning = showwarning
return log
else:
return None
def __exit__(self, *exc_info):
if not self._entered:
raise RuntimeError("Cannot exit %r without entering first" % self)
self._module.filters = self._filters
self._module.showwarning = self._showwarning
#=============================================================================
# eof
#=============================================================================

View File

@ -0,0 +1,9 @@
[passlib]
schemes = des_crypt, md5_crypt, bsdi_crypt, sha512_crypt
default = md5_crypt
all__vary_rounds = 0.1
bsdi_crypt__default_rounds = 25000
bsdi_crypt__max_rounds = 30000
sha512_crypt__max_rounds = 50000
sha512_crypt__min_rounds = 40000

View File

@ -0,0 +1,9 @@
[passlib]
schemes = des_crypt, md5_crypt, bsdi_crypt, sha512_crypt
default = md5_crypt
all__vary_rounds = 0.1
bsdi_crypt__default_rounds = 25000
bsdi_crypt__max_rounds = 30000
sha512_crypt__max_rounds = 50000
sha512_crypt__min_rounds = 40000

BIN
passlib/tests/sample1c.cfg Normal file

Binary file not shown.

View File

@ -0,0 +1,8 @@
[passlib]
schemes = des_crypt, md5_crypt, bsdi_crypt, sha512_crypt
default = md5_crypt
all.vary_rounds = 10%%
bsdi_crypt.max_rounds = 30000
bsdi_crypt.default_rounds = 25000
sha512_crypt.max_rounds = 50000
sha512_crypt.min_rounds = 40000

View File

@ -0,0 +1,564 @@
"""tests for passlib.apache -- (c) Assurance Technologies 2008-2011"""
#=============================================================================
# imports
#=============================================================================
from __future__ import with_statement
# core
import hashlib
from logging import getLogger
import os
import time
# site
# pkg
from passlib import apache
from passlib.utils.compat import irange, unicode
from passlib.tests.utils import TestCase, get_file, set_file, catch_warnings, ensure_mtime_changed
from passlib.utils.compat import b, bytes, u
# module
log = getLogger(__name__)
def backdate_file_mtime(path, offset=10):
"backdate file's mtime by specified amount"
# NOTE: this is used so we can test code which detects mtime changes,
# without having to actually *pause* for that long.
atime = os.path.getatime(path)
mtime = os.path.getmtime(path)-offset
os.utime(path, (atime, mtime))
#=============================================================================
# htpasswd
#=============================================================================
class HtpasswdFileTest(TestCase):
"test HtpasswdFile class"
descriptionPrefix = "HtpasswdFile"
# sample with 4 users
sample_01 = b('user2:2CHkkwa2AtqGs\n'
'user3:{SHA}3ipNV1GrBtxPmHFC21fCbVCSXIo=\n'
'user4:pass4\n'
'user1:$apr1$t4tc7jTh$GPIWVUo8sQKJlUdV8V5vu0\n')
# sample 1 with user 1, 2 deleted; 4 changed
sample_02 = b('user3:{SHA}3ipNV1GrBtxPmHFC21fCbVCSXIo=\nuser4:pass4\n')
# sample 1 with user2 updated, user 1 first entry removed, and user 5 added
sample_03 = b('user2:pass2x\n'
'user3:{SHA}3ipNV1GrBtxPmHFC21fCbVCSXIo=\n'
'user4:pass4\n'
'user1:$apr1$t4tc7jTh$GPIWVUo8sQKJlUdV8V5vu0\n'
'user5:pass5\n')
# standalone sample with 8-bit username
sample_04_utf8 = b('user\xc3\xa6:2CHkkwa2AtqGs\n')
sample_04_latin1 = b('user\xe6:2CHkkwa2AtqGs\n')
sample_dup = b('user1:pass1\nuser1:pass2\n')
def test_00_constructor_autoload(self):
"test constructor autoload"
# check with existing file
path = self.mktemp()
set_file(path, self.sample_01)
ht = apache.HtpasswdFile(path)
self.assertEqual(ht.to_string(), self.sample_01)
self.assertEqual(ht.path, path)
self.assertTrue(ht.mtime)
# check changing path
ht.path = path + "x"
self.assertEqual(ht.path, path + "x")
self.assertFalse(ht.mtime)
# check new=True
ht = apache.HtpasswdFile(path, new=True)
self.assertEqual(ht.to_string(), b(""))
self.assertEqual(ht.path, path)
self.assertFalse(ht.mtime)
# check autoload=False (deprecated alias for new=True)
with self.assertWarningList("``autoload=False`` is deprecated"):
ht = apache.HtpasswdFile(path, autoload=False)
self.assertEqual(ht.to_string(), b(""))
self.assertEqual(ht.path, path)
self.assertFalse(ht.mtime)
# check missing file
os.remove(path)
self.assertRaises(IOError, apache.HtpasswdFile, path)
# NOTE: "default_scheme" option checked via set_password() test, among others
def test_00_from_path(self):
path = self.mktemp()
set_file(path, self.sample_01)
ht = apache.HtpasswdFile.from_path(path)
self.assertEqual(ht.to_string(), self.sample_01)
self.assertEqual(ht.path, None)
self.assertFalse(ht.mtime)
def test_01_delete(self):
"test delete()"
ht = apache.HtpasswdFile.from_string(self.sample_01)
self.assertTrue(ht.delete("user1")) # should delete both entries
self.assertTrue(ht.delete("user2"))
self.assertFalse(ht.delete("user5")) # user not present
self.assertEqual(ht.to_string(), self.sample_02)
# invalid user
self.assertRaises(ValueError, ht.delete, "user:")
def test_01_delete_autosave(self):
path = self.mktemp()
sample = b('user1:pass1\nuser2:pass2\n')
set_file(path, sample)
ht = apache.HtpasswdFile(path)
ht.delete("user1")
self.assertEqual(get_file(path), sample)
ht = apache.HtpasswdFile(path, autosave=True)
ht.delete("user1")
self.assertEqual(get_file(path), b("user2:pass2\n"))
def test_02_set_password(self):
"test set_password()"
ht = apache.HtpasswdFile.from_string(
self.sample_01, default_scheme="plaintext")
self.assertTrue(ht.set_password("user2", "pass2x"))
self.assertFalse(ht.set_password("user5", "pass5"))
self.assertEqual(ht.to_string(), self.sample_03)
# test legacy default kwd
with self.assertWarningList("``default`` is deprecated"):
ht = apache.HtpasswdFile.from_string(self.sample_01, default="plaintext")
self.assertTrue(ht.set_password("user2", "pass2x"))
self.assertFalse(ht.set_password("user5", "pass5"))
self.assertEqual(ht.to_string(), self.sample_03)
# invalid user
self.assertRaises(ValueError, ht.set_password, "user:", "pass")
# test that legacy update() still works
with self.assertWarningList("update\(\) is deprecated"):
ht.update("user2", "test")
self.assertTrue(ht.check_password("user2", "test"))
def test_02_set_password_autosave(self):
path = self.mktemp()
sample = b('user1:pass1\n')
set_file(path, sample)
ht = apache.HtpasswdFile(path)
ht.set_password("user1", "pass2")
self.assertEqual(get_file(path), sample)
ht = apache.HtpasswdFile(path, default_scheme="plaintext", autosave=True)
ht.set_password("user1", "pass2")
self.assertEqual(get_file(path), b("user1:pass2\n"))
def test_03_users(self):
"test users()"
ht = apache.HtpasswdFile.from_string(self.sample_01)
ht.set_password("user5", "pass5")
ht.delete("user3")
ht.set_password("user3", "pass3")
self.assertEqual(ht.users(), ["user2", "user4", "user1", "user5",
"user3"])
def test_04_check_password(self):
"test check_password()"
ht = apache.HtpasswdFile.from_string(self.sample_01)
self.assertRaises(TypeError, ht.check_password, 1, 'pass5')
self.assertTrue(ht.check_password("user5","pass5") is None)
for i in irange(1,5):
i = str(i)
self.assertTrue(ht.check_password("user"+i, "pass"+i))
self.assertTrue(ht.check_password("user"+i, "pass5") is False)
self.assertRaises(ValueError, ht.check_password, "user:", "pass")
# test that legacy verify() still works
with self.assertWarningList(["verify\(\) is deprecated"]*2):
self.assertTrue(ht.verify("user1", "pass1"))
self.assertFalse(ht.verify("user1", "pass2"))
def test_05_load(self):
"test load()"
# setup empty file
path = self.mktemp()
set_file(path, "")
backdate_file_mtime(path, 5)
ha = apache.HtpasswdFile(path, default_scheme="plaintext")
self.assertEqual(ha.to_string(), b(""))
# make changes, check load_if_changed() does nothing
ha.set_password("user1", "pass1")
ha.load_if_changed()
self.assertEqual(ha.to_string(), b("user1:pass1\n"))
# change file
set_file(path, self.sample_01)
ha.load_if_changed()
self.assertEqual(ha.to_string(), self.sample_01)
# make changes, check load() overwrites them
ha.set_password("user5", "pass5")
ha.load()
self.assertEqual(ha.to_string(), self.sample_01)
# test load w/ no path
hb = apache.HtpasswdFile()
self.assertRaises(RuntimeError, hb.load)
self.assertRaises(RuntimeError, hb.load_if_changed)
# test load w/ dups and explicit path
set_file(path, self.sample_dup)
hc = apache.HtpasswdFile()
hc.load(path)
self.assertTrue(hc.check_password('user1','pass1'))
# NOTE: load_string() tested via from_string(), which is used all over this file
def test_06_save(self):
"test save()"
# load from file
path = self.mktemp()
set_file(path, self.sample_01)
ht = apache.HtpasswdFile(path)
# make changes, check they saved
ht.delete("user1")
ht.delete("user2")
ht.save()
self.assertEqual(get_file(path), self.sample_02)
# test save w/ no path
hb = apache.HtpasswdFile(default_scheme="plaintext")
hb.set_password("user1", "pass1")
self.assertRaises(RuntimeError, hb.save)
# test save w/ explicit path
hb.save(path)
self.assertEqual(get_file(path), b("user1:pass1\n"))
def test_07_encodings(self):
"test 'encoding' kwd"
# test bad encodings cause failure in constructor
self.assertRaises(ValueError, apache.HtpasswdFile, encoding="utf-16")
# check sample utf-8
ht = apache.HtpasswdFile.from_string(self.sample_04_utf8, encoding="utf-8",
return_unicode=True)
self.assertEqual(ht.users(), [ u("user\u00e6") ])
# test deprecated encoding=None
with self.assertWarningList("``encoding=None`` is deprecated"):
ht = apache.HtpasswdFile.from_string(self.sample_04_utf8, encoding=None)
self.assertEqual(ht.users(), [ b('user\xc3\xa6') ])
# check sample latin-1
ht = apache.HtpasswdFile.from_string(self.sample_04_latin1,
encoding="latin-1", return_unicode=True)
self.assertEqual(ht.users(), [ u("user\u00e6") ])
def test_08_get_hash(self):
"test get_hash()"
ht = apache.HtpasswdFile.from_string(self.sample_01)
self.assertEqual(ht.get_hash("user3"), b("{SHA}3ipNV1GrBtxPmHFC21fCbVCSXIo="))
self.assertEqual(ht.get_hash("user4"), b("pass4"))
self.assertEqual(ht.get_hash("user5"), None)
with self.assertWarningList("find\(\) is deprecated"):
self.assertEqual(ht.find("user4"), b("pass4"))
def test_09_to_string(self):
"test to_string"
# check with known sample
ht = apache.HtpasswdFile.from_string(self.sample_01)
self.assertEqual(ht.to_string(), self.sample_01)
# test blank
ht = apache.HtpasswdFile()
self.assertEqual(ht.to_string(), b(""))
def test_10_repr(self):
ht = apache.HtpasswdFile("fakepath", autosave=True, new=True, encoding="latin-1")
repr(ht)
def test_11_malformed(self):
self.assertRaises(ValueError, apache.HtpasswdFile.from_string,
b('realm:user1:pass1\n'))
self.assertRaises(ValueError, apache.HtpasswdFile.from_string,
b('pass1\n'))
def test_12_from_string(self):
# forbid path kwd
self.assertRaises(TypeError, apache.HtpasswdFile.from_string,
b(''), path=None)
#===================================================================
# eoc
#===================================================================
#=============================================================================
# htdigest
#=============================================================================
class HtdigestFileTest(TestCase):
"test HtdigestFile class"
descriptionPrefix = "HtdigestFile"
# sample with 4 users
sample_01 = b('user2:realm:549d2a5f4659ab39a80dac99e159ab19\n'
'user3:realm:a500bb8c02f6a9170ae46af10c898744\n'
'user4:realm:ab7b5d5f28ccc7666315f508c7358519\n'
'user1:realm:2a6cf53e7d8f8cf39d946dc880b14128\n')
# sample 1 with user 1, 2 deleted; 4 changed
sample_02 = b('user3:realm:a500bb8c02f6a9170ae46af10c898744\n'
'user4:realm:ab7b5d5f28ccc7666315f508c7358519\n')
# sample 1 with user2 updated, user 1 first entry removed, and user 5 added
sample_03 = b('user2:realm:5ba6d8328943c23c64b50f8b29566059\n'
'user3:realm:a500bb8c02f6a9170ae46af10c898744\n'
'user4:realm:ab7b5d5f28ccc7666315f508c7358519\n'
'user1:realm:2a6cf53e7d8f8cf39d946dc880b14128\n'
'user5:realm:03c55fdc6bf71552356ad401bdb9af19\n')
# standalone sample with 8-bit username & realm
sample_04_utf8 = b('user\xc3\xa6:realm\xc3\xa6:549d2a5f4659ab39a80dac99e159ab19\n')
sample_04_latin1 = b('user\xe6:realm\xe6:549d2a5f4659ab39a80dac99e159ab19\n')
def test_00_constructor_autoload(self):
"test constructor autoload"
# check with existing file
path = self.mktemp()
set_file(path, self.sample_01)
ht = apache.HtdigestFile(path)
self.assertEqual(ht.to_string(), self.sample_01)
# check without autoload
ht = apache.HtdigestFile(path, new=True)
self.assertEqual(ht.to_string(), b(""))
# check missing file
os.remove(path)
self.assertRaises(IOError, apache.HtdigestFile, path)
# NOTE: default_realm option checked via other tests.
def test_01_delete(self):
"test delete()"
ht = apache.HtdigestFile.from_string(self.sample_01)
self.assertTrue(ht.delete("user1", "realm"))
self.assertTrue(ht.delete("user2", "realm"))
self.assertFalse(ht.delete("user5", "realm"))
self.assertFalse(ht.delete("user3", "realm5"))
self.assertEqual(ht.to_string(), self.sample_02)
# invalid user
self.assertRaises(ValueError, ht.delete, "user:", "realm")
# invalid realm
self.assertRaises(ValueError, ht.delete, "user", "realm:")
def test_01_delete_autosave(self):
path = self.mktemp()
set_file(path, self.sample_01)
ht = apache.HtdigestFile(path)
self.assertTrue(ht.delete("user1", "realm"))
self.assertFalse(ht.delete("user3", "realm5"))
self.assertFalse(ht.delete("user5", "realm"))
self.assertEqual(get_file(path), self.sample_01)
ht.autosave = True
self.assertTrue(ht.delete("user2", "realm"))
self.assertEqual(get_file(path), self.sample_02)
def test_02_set_password(self):
"test update()"
ht = apache.HtdigestFile.from_string(self.sample_01)
self.assertTrue(ht.set_password("user2", "realm", "pass2x"))
self.assertFalse(ht.set_password("user5", "realm", "pass5"))
self.assertEqual(ht.to_string(), self.sample_03)
# default realm
self.assertRaises(TypeError, ht.set_password, "user2", "pass3")
ht.default_realm = "realm2"
ht.set_password("user2", "pass3")
ht.check_password("user2", "realm2", "pass3")
# invalid user
self.assertRaises(ValueError, ht.set_password, "user:", "realm", "pass")
self.assertRaises(ValueError, ht.set_password, "u"*256, "realm", "pass")
# invalid realm
self.assertRaises(ValueError, ht.set_password, "user", "realm:", "pass")
self.assertRaises(ValueError, ht.set_password, "user", "r"*256, "pass")
# test that legacy update() still works
with self.assertWarningList("update\(\) is deprecated"):
ht.update("user2", "realm2", "test")
self.assertTrue(ht.check_password("user2", "test"))
# TODO: test set_password autosave
def test_03_users(self):
"test users()"
ht = apache.HtdigestFile.from_string(self.sample_01)
ht.set_password("user5", "realm", "pass5")
ht.delete("user3", "realm")
ht.set_password("user3", "realm", "pass3")
self.assertEqual(ht.users("realm"), ["user2", "user4", "user1", "user5", "user3"])
self.assertRaises(TypeError, ht.users, 1)
def test_04_check_password(self):
"test check_password()"
ht = apache.HtdigestFile.from_string(self.sample_01)
self.assertRaises(TypeError, ht.check_password, 1, 'realm', 'pass5')
self.assertRaises(TypeError, ht.check_password, 'user', 1, 'pass5')
self.assertIs(ht.check_password("user5", "realm","pass5"), None)
for i in irange(1,5):
i = str(i)
self.assertTrue(ht.check_password("user"+i, "realm", "pass"+i))
self.assertIs(ht.check_password("user"+i, "realm", "pass5"), False)
# default realm
self.assertRaises(TypeError, ht.check_password, "user5", "pass5")
ht.default_realm = "realm"
self.assertTrue(ht.check_password("user1", "pass1"))
self.assertIs(ht.check_password("user5", "pass5"), None)
# test that legacy verify() still works
with self.assertWarningList(["verify\(\) is deprecated"]*2):
self.assertTrue(ht.verify("user1", "realm", "pass1"))
self.assertFalse(ht.verify("user1", "realm", "pass2"))
# invalid user
self.assertRaises(ValueError, ht.check_password, "user:", "realm", "pass")
def test_05_load(self):
"test load()"
# setup empty file
path = self.mktemp()
set_file(path, "")
backdate_file_mtime(path, 5)
ha = apache.HtdigestFile(path)
self.assertEqual(ha.to_string(), b(""))
# make changes, check load_if_changed() does nothing
ha.set_password("user1", "realm", "pass1")
ha.load_if_changed()
self.assertEqual(ha.to_string(), b('user1:realm:2a6cf53e7d8f8cf39d946dc880b14128\n'))
# change file
set_file(path, self.sample_01)
ha.load_if_changed()
self.assertEqual(ha.to_string(), self.sample_01)
# make changes, check load_if_changed overwrites them
ha.set_password("user5", "realm", "pass5")
ha.load()
self.assertEqual(ha.to_string(), self.sample_01)
# test load w/ no path
hb = apache.HtdigestFile()
self.assertRaises(RuntimeError, hb.load)
self.assertRaises(RuntimeError, hb.load_if_changed)
# test load w/ explicit path
hc = apache.HtdigestFile()
hc.load(path)
self.assertEqual(hc.to_string(), self.sample_01)
# change file, test deprecated force=False kwd
ensure_mtime_changed(path)
set_file(path, "")
with self.assertWarningList(r"load\(force=False\) is deprecated"):
ha.load(force=False)
self.assertEqual(ha.to_string(), b(""))
def test_06_save(self):
"test save()"
# load from file
path = self.mktemp()
set_file(path, self.sample_01)
ht = apache.HtdigestFile(path)
# make changes, check they saved
ht.delete("user1", "realm")
ht.delete("user2", "realm")
ht.save()
self.assertEqual(get_file(path), self.sample_02)
# test save w/ no path
hb = apache.HtdigestFile()
hb.set_password("user1", "realm", "pass1")
self.assertRaises(RuntimeError, hb.save)
# test save w/ explicit path
hb.save(path)
self.assertEqual(get_file(path), hb.to_string())
def test_07_realms(self):
"test realms() & delete_realm()"
ht = apache.HtdigestFile.from_string(self.sample_01)
self.assertEqual(ht.delete_realm("x"), 0)
self.assertEqual(ht.realms(), ['realm'])
self.assertEqual(ht.delete_realm("realm"), 4)
self.assertEqual(ht.realms(), [])
self.assertEqual(ht.to_string(), b(""))
def test_08_get_hash(self):
"test get_hash()"
ht = apache.HtdigestFile.from_string(self.sample_01)
self.assertEqual(ht.get_hash("user3", "realm"), "a500bb8c02f6a9170ae46af10c898744")
self.assertEqual(ht.get_hash("user4", "realm"), "ab7b5d5f28ccc7666315f508c7358519")
self.assertEqual(ht.get_hash("user5", "realm"), None)
with self.assertWarningList("find\(\) is deprecated"):
self.assertEqual(ht.find("user4", "realm"), "ab7b5d5f28ccc7666315f508c7358519")
def test_09_encodings(self):
"test encoding parameter"
# test bad encodings cause failure in constructor
self.assertRaises(ValueError, apache.HtdigestFile, encoding="utf-16")
# check sample utf-8
ht = apache.HtdigestFile.from_string(self.sample_04_utf8, encoding="utf-8", return_unicode=True)
self.assertEqual(ht.realms(), [ u("realm\u00e6") ])
self.assertEqual(ht.users(u("realm\u00e6")), [ u("user\u00e6") ])
# check sample latin-1
ht = apache.HtdigestFile.from_string(self.sample_04_latin1, encoding="latin-1", return_unicode=True)
self.assertEqual(ht.realms(), [ u("realm\u00e6") ])
self.assertEqual(ht.users(u("realm\u00e6")), [ u("user\u00e6") ])
def test_10_to_string(self):
"test to_string()"
# check sample
ht = apache.HtdigestFile.from_string(self.sample_01)
self.assertEqual(ht.to_string(), self.sample_01)
# check blank
ht = apache.HtdigestFile()
self.assertEqual(ht.to_string(), b(""))
def test_11_malformed(self):
self.assertRaises(ValueError, apache.HtdigestFile.from_string,
b('realm:user1:pass1:other\n'))
self.assertRaises(ValueError, apache.HtdigestFile.from_string,
b('user1:pass1\n'))
#===================================================================
# eoc
#===================================================================
#=============================================================================
# eof
#=============================================================================

128
passlib/tests/test_apps.py Normal file
View File

@ -0,0 +1,128 @@
"""test passlib.apps"""
#=============================================================================
# imports
#=============================================================================
from __future__ import with_statement
# core
import logging; log = logging.getLogger(__name__)
# site
# pkg
from passlib import apps, hash as hashmod
from passlib.tests.utils import TestCase
# module
#=============================================================================
# test predefined app contexts
#=============================================================================
class AppsTest(TestCase):
"perform general tests to make sure contexts work"
# NOTE: these tests are not really comprehensive,
# since they would do little but duplicate
# the presets in apps.py
#
# they mainly try to ensure no typos
# or dynamic behavior foul-ups.
def test_master_context(self):
ctx = apps.master_context
self.assertGreater(len(ctx.schemes()), 50)
def test_custom_app_context(self):
ctx = apps.custom_app_context
self.assertEqual(ctx.schemes(), ("sha512_crypt", "sha256_crypt"))
for hash in [
('$6$rounds=41128$VoQLvDjkaZ6L6BIE$4pt.1Ll1XdDYduEwEYPCMOBiR6W6'
'znsyUEoNlcVXpv2gKKIbQolgmTGe6uEEVJ7azUxuc8Tf7zV9SD2z7Ij751'),
('$5$rounds=31817$iZGmlyBQ99JSB5n6$p4E.pdPBWx19OajgjLRiOW0itGny'
'xDGgMlDcOsfaI17'),
]:
self.assertTrue(ctx.verify("test", hash))
def test_django_context(self):
ctx = apps.django_context
for hash in [
'sha1$0d082$cdb462ae8b6be8784ef24b20778c4d0c82d5957f',
'md5$b887a$37767f8a745af10612ad44c80ff52e92',
'crypt$95a6d$95x74hLDQKXI2',
'098f6bcd4621d373cade4e832627b4f6',
]:
self.assertTrue(ctx.verify("test", hash))
self.assertEqual(ctx.identify("!"), "django_disabled")
self.assertFalse(ctx.verify("test", "!"))
def test_ldap_nocrypt_context(self):
ctx = apps.ldap_nocrypt_context
for hash in [
'{SSHA}cPusOzd6d5n3OjSVK3R329ZGCNyFcC7F',
'test',
]:
self.assertTrue(ctx.verify("test", hash))
self.assertIs(ctx.identify('{CRYPT}$5$rounds=31817$iZGmlyBQ99JSB5'
'n6$p4E.pdPBWx19OajgjLRiOW0itGnyxDGgMlDcOsfaI17'), None)
def test_ldap_context(self):
ctx = apps.ldap_context
for hash in [
('{CRYPT}$5$rounds=31817$iZGmlyBQ99JSB5n6$p4E.pdPBWx19OajgjLRiOW0'
'itGnyxDGgMlDcOsfaI17'),
'{SSHA}cPusOzd6d5n3OjSVK3R329ZGCNyFcC7F',
'test',
]:
self.assertTrue(ctx.verify("test", hash))
def test_ldap_mysql_context(self):
ctx = apps.mysql_context
for hash in [
'*94BDCEBE19083CE2A1F959FD02F964C7AF4CFC29',
'378b243e220ca493',
]:
self.assertTrue(ctx.verify("test", hash))
def test_postgres_context(self):
ctx = apps.postgres_context
hash = 'md55d9c68c6c50ed3d02a2fcf54f63993b6'
self.assertTrue(ctx.verify("test", hash, user='user'))
def test_phppass_context(self):
ctx = apps.phpass_context
for hash in [
'$P$8Ja1vJsKa5qyy/b3mCJGXM7GyBnt6..',
'$H$8b95CoYQnQ9Y6fSTsACyphNh5yoM02.',
'_cD..aBxeRhYFJvtUvsI',
]:
self.assertTrue(ctx.verify("test", hash))
h1 = "$2a$04$yjDgE74RJkeqC0/1NheSSOrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIS"
if hashmod.bcrypt.has_backend():
self.assertTrue(ctx.verify("test", h1))
self.assertEqual(ctx.default_scheme(), "bcrypt")
self.assertEqual(ctx.handler().name, "bcrypt")
else:
self.assertEqual(ctx.identify(h1), "bcrypt")
self.assertEqual(ctx.default_scheme(), "phpass")
self.assertEqual(ctx.handler().name, "phpass")
def test_phpbb3_context(self):
ctx = apps.phpbb3_context
for hash in [
'$P$8Ja1vJsKa5qyy/b3mCJGXM7GyBnt6..',
'$H$8b95CoYQnQ9Y6fSTsACyphNh5yoM02.',
]:
self.assertTrue(ctx.verify("test", hash))
self.assertTrue(ctx.encrypt("test").startswith("$H$"))
def test_roundup_context(self):
ctx = apps.roundup_context
for hash in [
'{PBKDF2}9849$JMTYu3eOUSoFYExprVVqbQ$N5.gV.uR1.BTgLSvi0qyPiRlGZ0',
'{SHA}a94a8fe5ccb19ba61c4c0873d391e987982fbbd3',
'{CRYPT}dptOmKDriOGfU',
'{plaintext}test',
]:
self.assertTrue(ctx.verify("test", hash))
#=============================================================================
# eof
#=============================================================================

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,752 @@
"""tests for passlib.context
this file is a clone of the 1.5 test_context.py,
containing the tests using the legacy CryptPolicy api.
it's being preserved here to ensure the old api doesn't break
(until Passlib 1.8, when this and the legacy api will be removed).
"""
#=============================================================================
# imports
#=============================================================================
from __future__ import with_statement
# core
import hashlib
from logging import getLogger
import os
import time
import warnings
import sys
# site
try:
from pkg_resources import resource_filename
except ImportError:
resource_filename = None
# pkg
from passlib import hash
from passlib.context import CryptContext, CryptPolicy, LazyCryptContext
from passlib.exc import PasslibConfigWarning
from passlib.utils import tick, to_bytes, to_unicode
from passlib.utils.compat import irange, u, bytes
import passlib.utils.handlers as uh
from passlib.tests.utils import TestCase, catch_warnings, set_file
from passlib.registry import (register_crypt_handler_path,
_has_crypt_handler as has_crypt_handler,
_unload_handler_name as unload_handler_name,
get_crypt_handler,
)
# module
log = getLogger(__name__)
#=============================================================================
#
#=============================================================================
class CryptPolicyTest(TestCase):
"test CryptPolicy object"
# TODO: need to test user categories w/in all this
descriptionPrefix = "CryptPolicy"
#===================================================================
# sample crypt policies used for testing
#===================================================================
#---------------------------------------------------------------
# sample 1 - average config file
#---------------------------------------------------------------
# NOTE: copy of this is stored in file passlib/tests/sample_config_1s.cfg
sample_config_1s = """\
[passlib]
schemes = des_crypt, md5_crypt, bsdi_crypt, sha512_crypt
default = md5_crypt
all.vary_rounds = 10%%
bsdi_crypt.max_rounds = 30000
bsdi_crypt.default_rounds = 25000
sha512_crypt.max_rounds = 50000
sha512_crypt.min_rounds = 40000
"""
sample_config_1s_path = os.path.abspath(os.path.join(
os.path.dirname(__file__), "sample_config_1s.cfg"))
if not os.path.exists(sample_config_1s_path) and resource_filename:
# in case we're zipped up in an egg.
sample_config_1s_path = resource_filename("passlib.tests",
"sample_config_1s.cfg")
# make sure sample_config_1s uses \n linesep - tests rely on this
assert sample_config_1s.startswith("[passlib]\nschemes")
sample_config_1pd = dict(
schemes = [ "des_crypt", "md5_crypt", "bsdi_crypt", "sha512_crypt"],
default = "md5_crypt",
# NOTE: not maintaining backwards compat for rendering to "10%"
all__vary_rounds = 0.1,
bsdi_crypt__max_rounds = 30000,
bsdi_crypt__default_rounds = 25000,
sha512_crypt__max_rounds = 50000,
sha512_crypt__min_rounds = 40000,
)
sample_config_1pid = {
"schemes": "des_crypt, md5_crypt, bsdi_crypt, sha512_crypt",
"default": "md5_crypt",
# NOTE: not maintaining backwards compat for rendering to "10%"
"all.vary_rounds": 0.1,
"bsdi_crypt.max_rounds": 30000,
"bsdi_crypt.default_rounds": 25000,
"sha512_crypt.max_rounds": 50000,
"sha512_crypt.min_rounds": 40000,
}
sample_config_1prd = dict(
schemes = [ hash.des_crypt, hash.md5_crypt, hash.bsdi_crypt, hash.sha512_crypt],
default = "md5_crypt", # NOTE: passlib <= 1.5 was handler obj.
# NOTE: not maintaining backwards compat for rendering to "10%"
all__vary_rounds = 0.1,
bsdi_crypt__max_rounds = 30000,
bsdi_crypt__default_rounds = 25000,
sha512_crypt__max_rounds = 50000,
sha512_crypt__min_rounds = 40000,
)
#---------------------------------------------------------------
# sample 2 - partial policy & result of overlay on sample 1
#---------------------------------------------------------------
sample_config_2s = """\
[passlib]
bsdi_crypt.min_rounds = 29000
bsdi_crypt.max_rounds = 35000
bsdi_crypt.default_rounds = 31000
sha512_crypt.min_rounds = 45000
"""
sample_config_2pd = dict(
# using this to test full replacement of existing options
bsdi_crypt__min_rounds = 29000,
bsdi_crypt__max_rounds = 35000,
bsdi_crypt__default_rounds = 31000,
# using this to test partial replacement of existing options
sha512_crypt__min_rounds=45000,
)
sample_config_12pd = dict(
schemes = [ "des_crypt", "md5_crypt", "bsdi_crypt", "sha512_crypt"],
default = "md5_crypt",
# NOTE: not maintaining backwards compat for rendering to "10%"
all__vary_rounds = 0.1,
bsdi_crypt__min_rounds = 29000,
bsdi_crypt__max_rounds = 35000,
bsdi_crypt__default_rounds = 31000,
sha512_crypt__max_rounds = 50000,
sha512_crypt__min_rounds=45000,
)
#---------------------------------------------------------------
# sample 3 - just changing default
#---------------------------------------------------------------
sample_config_3pd = dict(
default="sha512_crypt",
)
sample_config_123pd = dict(
schemes = [ "des_crypt", "md5_crypt", "bsdi_crypt", "sha512_crypt"],
default = "sha512_crypt",
# NOTE: not maintaining backwards compat for rendering to "10%"
all__vary_rounds = 0.1,
bsdi_crypt__min_rounds = 29000,
bsdi_crypt__max_rounds = 35000,
bsdi_crypt__default_rounds = 31000,
sha512_crypt__max_rounds = 50000,
sha512_crypt__min_rounds=45000,
)
#---------------------------------------------------------------
# sample 4 - category specific
#---------------------------------------------------------------
sample_config_4s = """
[passlib]
schemes = sha512_crypt
all.vary_rounds = 10%%
default.sha512_crypt.max_rounds = 20000
admin.all.vary_rounds = 5%%
admin.sha512_crypt.max_rounds = 40000
"""
sample_config_4pd = dict(
schemes = [ "sha512_crypt" ],
# NOTE: not maintaining backwards compat for rendering to "10%"
all__vary_rounds = 0.1,
sha512_crypt__max_rounds = 20000,
# NOTE: not maintaining backwards compat for rendering to "5%"
admin__all__vary_rounds = 0.05,
admin__sha512_crypt__max_rounds = 40000,
)
#---------------------------------------------------------------
# sample 5 - to_string & deprecation testing
#---------------------------------------------------------------
sample_config_5s = sample_config_1s + """\
deprecated = des_crypt
admin__context__deprecated = des_crypt, bsdi_crypt
"""
sample_config_5pd = sample_config_1pd.copy()
sample_config_5pd.update(
deprecated = [ "des_crypt" ],
admin__context__deprecated = [ "des_crypt", "bsdi_crypt" ],
)
sample_config_5pid = sample_config_1pid.copy()
sample_config_5pid.update({
"deprecated": "des_crypt",
"admin.context.deprecated": "des_crypt, bsdi_crypt",
})
sample_config_5prd = sample_config_1prd.copy()
sample_config_5prd.update({
# XXX: should deprecated return the actual handlers in this case?
# would have to modify how policy stores info, for one.
"deprecated": ["des_crypt"],
"admin__context__deprecated": ["des_crypt", "bsdi_crypt"],
})
#===================================================================
# constructors
#===================================================================
def setUp(self):
TestCase.setUp(self)
warnings.filterwarnings("ignore",
r"The CryptPolicy class has been deprecated")
warnings.filterwarnings("ignore",
r"the method.*hash_needs_update.*is deprecated")
def test_00_constructor(self):
"test CryptPolicy() constructor"
policy = CryptPolicy(**self.sample_config_1pd)
self.assertEqual(policy.to_dict(), self.sample_config_1pd)
policy = CryptPolicy(self.sample_config_1pd)
self.assertEqual(policy.to_dict(), self.sample_config_1pd)
self.assertRaises(TypeError, CryptPolicy, {}, {})
self.assertRaises(TypeError, CryptPolicy, {}, dummy=1)
# check key with too many separators is rejected
self.assertRaises(TypeError, CryptPolicy,
schemes = [ "des_crypt", "md5_crypt", "bsdi_crypt", "sha512_crypt"],
bad__key__bsdi_crypt__max_rounds = 30000,
)
# check nameless handler rejected
class nameless(uh.StaticHandler):
name = None
self.assertRaises(ValueError, CryptPolicy, schemes=[nameless])
# check scheme must be name or crypt handler
self.assertRaises(TypeError, CryptPolicy, schemes=[uh.StaticHandler])
# check name conflicts are rejected
class dummy_1(uh.StaticHandler):
name = 'dummy_1'
self.assertRaises(KeyError, CryptPolicy, schemes=[dummy_1, dummy_1])
# with unknown deprecated value
self.assertRaises(KeyError, CryptPolicy,
schemes=['des_crypt'],
deprecated=['md5_crypt'])
# with unknown default value
self.assertRaises(KeyError, CryptPolicy,
schemes=['des_crypt'],
default='md5_crypt')
def test_01_from_path_simple(self):
"test CryptPolicy.from_path() constructor"
# NOTE: this is separate so it can also run under GAE
# test preset stored in existing file
path = self.sample_config_1s_path
policy = CryptPolicy.from_path(path)
self.assertEqual(policy.to_dict(), self.sample_config_1pd)
# test if path missing
self.assertRaises(EnvironmentError, CryptPolicy.from_path, path + 'xxx')
def test_01_from_path(self):
"test CryptPolicy.from_path() constructor with encodings"
path = self.mktemp()
# test "\n" linesep
set_file(path, self.sample_config_1s)
policy = CryptPolicy.from_path(path)
self.assertEqual(policy.to_dict(), self.sample_config_1pd)
# test "\r\n" linesep
set_file(path, self.sample_config_1s.replace("\n","\r\n"))
policy = CryptPolicy.from_path(path)
self.assertEqual(policy.to_dict(), self.sample_config_1pd)
# test with custom encoding
uc2 = to_bytes(self.sample_config_1s, "utf-16", source_encoding="utf-8")
set_file(path, uc2)
policy = CryptPolicy.from_path(path, encoding="utf-16")
self.assertEqual(policy.to_dict(), self.sample_config_1pd)
def test_02_from_string(self):
"test CryptPolicy.from_string() constructor"
# test "\n" linesep
policy = CryptPolicy.from_string(self.sample_config_1s)
self.assertEqual(policy.to_dict(), self.sample_config_1pd)
# test "\r\n" linesep
policy = CryptPolicy.from_string(
self.sample_config_1s.replace("\n","\r\n"))
self.assertEqual(policy.to_dict(), self.sample_config_1pd)
# test with unicode
data = to_unicode(self.sample_config_1s)
policy = CryptPolicy.from_string(data)
self.assertEqual(policy.to_dict(), self.sample_config_1pd)
# test with non-ascii-compatible encoding
uc2 = to_bytes(self.sample_config_1s, "utf-16", source_encoding="utf-8")
policy = CryptPolicy.from_string(uc2, encoding="utf-16")
self.assertEqual(policy.to_dict(), self.sample_config_1pd)
# test category specific options
policy = CryptPolicy.from_string(self.sample_config_4s)
self.assertEqual(policy.to_dict(), self.sample_config_4pd)
def test_03_from_source(self):
"test CryptPolicy.from_source() constructor"
# pass it a path
policy = CryptPolicy.from_source(self.sample_config_1s_path)
self.assertEqual(policy.to_dict(), self.sample_config_1pd)
# pass it a string
policy = CryptPolicy.from_source(self.sample_config_1s)
self.assertEqual(policy.to_dict(), self.sample_config_1pd)
# pass it a dict (NOTE: make a copy to detect in-place modifications)
policy = CryptPolicy.from_source(self.sample_config_1pd.copy())
self.assertEqual(policy.to_dict(), self.sample_config_1pd)
# pass it existing policy
p2 = CryptPolicy.from_source(policy)
self.assertIs(policy, p2)
# pass it something wrong
self.assertRaises(TypeError, CryptPolicy.from_source, 1)
self.assertRaises(TypeError, CryptPolicy.from_source, [])
def test_04_from_sources(self):
"test CryptPolicy.from_sources() constructor"
# pass it empty list
self.assertRaises(ValueError, CryptPolicy.from_sources, [])
# pass it one-element list
policy = CryptPolicy.from_sources([self.sample_config_1s])
self.assertEqual(policy.to_dict(), self.sample_config_1pd)
# pass multiple sources
policy = CryptPolicy.from_sources(
[
self.sample_config_1s_path,
self.sample_config_2s,
self.sample_config_3pd,
])
self.assertEqual(policy.to_dict(), self.sample_config_123pd)
def test_05_replace(self):
"test CryptPolicy.replace() constructor"
p1 = CryptPolicy(**self.sample_config_1pd)
# check overlaying sample 2
p2 = p1.replace(**self.sample_config_2pd)
self.assertEqual(p2.to_dict(), self.sample_config_12pd)
# check repeating overlay makes no change
p2b = p2.replace(**self.sample_config_2pd)
self.assertEqual(p2b.to_dict(), self.sample_config_12pd)
# check overlaying sample 3
p3 = p2.replace(self.sample_config_3pd)
self.assertEqual(p3.to_dict(), self.sample_config_123pd)
def test_06_forbidden(self):
"test CryptPolicy() forbidden kwds"
# salt not allowed to be set
self.assertRaises(KeyError, CryptPolicy,
schemes=["des_crypt"],
des_crypt__salt="xx",
)
self.assertRaises(KeyError, CryptPolicy,
schemes=["des_crypt"],
all__salt="xx",
)
# schemes not allowed for category
self.assertRaises(KeyError, CryptPolicy,
schemes=["des_crypt"],
user__context__schemes=["md5_crypt"],
)
#===================================================================
# reading
#===================================================================
def test_10_has_schemes(self):
"test has_schemes() method"
p1 = CryptPolicy(**self.sample_config_1pd)
self.assertTrue(p1.has_schemes())
p3 = CryptPolicy(**self.sample_config_3pd)
self.assertTrue(not p3.has_schemes())
def test_11_iter_handlers(self):
"test iter_handlers() method"
p1 = CryptPolicy(**self.sample_config_1pd)
s = self.sample_config_1prd['schemes']
self.assertEqual(list(p1.iter_handlers()), s)
p3 = CryptPolicy(**self.sample_config_3pd)
self.assertEqual(list(p3.iter_handlers()), [])
def test_12_get_handler(self):
"test get_handler() method"
p1 = CryptPolicy(**self.sample_config_1pd)
# check by name
self.assertIs(p1.get_handler("bsdi_crypt"), hash.bsdi_crypt)
# check by missing name
self.assertIs(p1.get_handler("sha256_crypt"), None)
self.assertRaises(KeyError, p1.get_handler, "sha256_crypt", required=True)
# check default
self.assertIs(p1.get_handler(), hash.md5_crypt)
def test_13_get_options(self):
"test get_options() method"
p12 = CryptPolicy(**self.sample_config_12pd)
self.assertEqual(p12.get_options("bsdi_crypt"),dict(
# NOTE: not maintaining backwards compat for rendering to "10%"
vary_rounds = 0.1,
min_rounds = 29000,
max_rounds = 35000,
default_rounds = 31000,
))
self.assertEqual(p12.get_options("sha512_crypt"),dict(
# NOTE: not maintaining backwards compat for rendering to "10%"
vary_rounds = 0.1,
min_rounds = 45000,
max_rounds = 50000,
))
p4 = CryptPolicy.from_string(self.sample_config_4s)
self.assertEqual(p4.get_options("sha512_crypt"), dict(
# NOTE: not maintaining backwards compat for rendering to "10%"
vary_rounds=0.1,
max_rounds=20000,
))
self.assertEqual(p4.get_options("sha512_crypt", "user"), dict(
# NOTE: not maintaining backwards compat for rendering to "10%"
vary_rounds=0.1,
max_rounds=20000,
))
self.assertEqual(p4.get_options("sha512_crypt", "admin"), dict(
# NOTE: not maintaining backwards compat for rendering to "5%"
vary_rounds=0.05,
max_rounds=40000,
))
def test_14_handler_is_deprecated(self):
"test handler_is_deprecated() method"
pa = CryptPolicy(**self.sample_config_1pd)
pb = CryptPolicy(**self.sample_config_5pd)
self.assertFalse(pa.handler_is_deprecated("des_crypt"))
self.assertFalse(pa.handler_is_deprecated(hash.bsdi_crypt))
self.assertFalse(pa.handler_is_deprecated("sha512_crypt"))
self.assertTrue(pb.handler_is_deprecated("des_crypt"))
self.assertFalse(pb.handler_is_deprecated(hash.bsdi_crypt))
self.assertFalse(pb.handler_is_deprecated("sha512_crypt"))
# check categories as well
self.assertTrue(pb.handler_is_deprecated("des_crypt", "user"))
self.assertFalse(pb.handler_is_deprecated("bsdi_crypt", "user"))
self.assertTrue(pb.handler_is_deprecated("des_crypt", "admin"))
self.assertTrue(pb.handler_is_deprecated("bsdi_crypt", "admin"))
# check deprecation is overridden per category
pc = CryptPolicy(
schemes=["md5_crypt", "des_crypt"],
deprecated=["md5_crypt"],
user__context__deprecated=["des_crypt"],
)
self.assertTrue(pc.handler_is_deprecated("md5_crypt"))
self.assertFalse(pc.handler_is_deprecated("des_crypt"))
self.assertFalse(pc.handler_is_deprecated("md5_crypt", "user"))
self.assertTrue(pc.handler_is_deprecated("des_crypt", "user"))
def test_15_min_verify_time(self):
"test get_min_verify_time() method"
# silence deprecation warnings for min verify time
warnings.filterwarnings("ignore", category=DeprecationWarning)
pa = CryptPolicy()
self.assertEqual(pa.get_min_verify_time(), 0)
self.assertEqual(pa.get_min_verify_time('admin'), 0)
pb = pa.replace(min_verify_time=.1)
self.assertEqual(pb.get_min_verify_time(), .1)
self.assertEqual(pb.get_min_verify_time('admin'), .1)
pc = pa.replace(admin__context__min_verify_time=.2)
self.assertEqual(pc.get_min_verify_time(), 0)
self.assertEqual(pc.get_min_verify_time('admin'), .2)
pd = pb.replace(admin__context__min_verify_time=.2)
self.assertEqual(pd.get_min_verify_time(), .1)
self.assertEqual(pd.get_min_verify_time('admin'), .2)
#===================================================================
# serialization
#===================================================================
def test_20_iter_config(self):
"test iter_config() method"
p5 = CryptPolicy(**self.sample_config_5pd)
self.assertEqual(dict(p5.iter_config()), self.sample_config_5pd)
self.assertEqual(dict(p5.iter_config(resolve=True)), self.sample_config_5prd)
self.assertEqual(dict(p5.iter_config(ini=True)), self.sample_config_5pid)
def test_21_to_dict(self):
"test to_dict() method"
p5 = CryptPolicy(**self.sample_config_5pd)
self.assertEqual(p5.to_dict(), self.sample_config_5pd)
self.assertEqual(p5.to_dict(resolve=True), self.sample_config_5prd)
def test_22_to_string(self):
"test to_string() method"
pa = CryptPolicy(**self.sample_config_5pd)
s = pa.to_string() # NOTE: can't compare string directly, ordering etc may not match
pb = CryptPolicy.from_string(s)
self.assertEqual(pb.to_dict(), self.sample_config_5pd)
s = pa.to_string(encoding="latin-1")
self.assertIsInstance(s, bytes)
#===================================================================
#
#===================================================================
#=============================================================================
# CryptContext
#=============================================================================
class CryptContextTest(TestCase):
"test CryptContext class"
descriptionPrefix = "CryptContext"
def setUp(self):
TestCase.setUp(self)
warnings.filterwarnings("ignore",
r"CryptContext\(\)\.replace\(\) has been deprecated.*")
warnings.filterwarnings("ignore",
r"The CryptContext ``policy`` keyword has been deprecated.*")
warnings.filterwarnings("ignore", ".*(CryptPolicy|context\.policy).*(has|have) been deprecated.*")
warnings.filterwarnings("ignore",
r"the method.*hash_needs_update.*is deprecated")
#===================================================================
# constructor
#===================================================================
def test_00_constructor(self):
"test constructor"
# create crypt context using handlers
cc = CryptContext([hash.md5_crypt, hash.bsdi_crypt, hash.des_crypt])
c,b,a = cc.policy.iter_handlers()
self.assertIs(a, hash.des_crypt)
self.assertIs(b, hash.bsdi_crypt)
self.assertIs(c, hash.md5_crypt)
# create context using names
cc = CryptContext(["md5_crypt", "bsdi_crypt", "des_crypt"])
c,b,a = cc.policy.iter_handlers()
self.assertIs(a, hash.des_crypt)
self.assertIs(b, hash.bsdi_crypt)
self.assertIs(c, hash.md5_crypt)
# policy kwd
policy = cc.policy
cc = CryptContext(policy=policy)
self.assertEqual(cc.to_dict(), policy.to_dict())
cc = CryptContext(policy=policy, default="bsdi_crypt")
self.assertNotEqual(cc.to_dict(), policy.to_dict())
self.assertEqual(cc.to_dict(), dict(schemes=["md5_crypt","bsdi_crypt","des_crypt"],
default="bsdi_crypt"))
self.assertRaises(TypeError, setattr, cc, 'policy', None)
self.assertRaises(TypeError, CryptContext, policy='x')
def test_01_replace(self):
"test replace()"
cc = CryptContext(["md5_crypt", "bsdi_crypt", "des_crypt"])
self.assertIs(cc.policy.get_handler(), hash.md5_crypt)
cc2 = cc.replace()
self.assertIsNot(cc2, cc)
# NOTE: was not able to maintain backward compatibility with this...
##self.assertIs(cc2.policy, cc.policy)
cc3 = cc.replace(default="bsdi_crypt")
self.assertIsNot(cc3, cc)
# NOTE: was not able to maintain backward compatibility with this...
##self.assertIs(cc3.policy, cc.policy)
self.assertIs(cc3.policy.get_handler(), hash.bsdi_crypt)
def test_02_no_handlers(self):
"test no handlers"
# check constructor...
cc = CryptContext()
self.assertRaises(KeyError, cc.identify, 'hash', required=True)
self.assertRaises(KeyError, cc.encrypt, 'secret')
self.assertRaises(KeyError, cc.verify, 'secret', 'hash')
# check updating policy after the fact...
cc = CryptContext(['md5_crypt'])
p = CryptPolicy(schemes=[])
cc.policy = p
self.assertRaises(KeyError, cc.identify, 'hash', required=True)
self.assertRaises(KeyError, cc.encrypt, 'secret')
self.assertRaises(KeyError, cc.verify, 'secret', 'hash')
#===================================================================
# policy adaptation
#===================================================================
sample_policy_1 = dict(
schemes = [ "des_crypt", "md5_crypt", "phpass", "bsdi_crypt",
"sha256_crypt"],
deprecated = [ "des_crypt", ],
default = "sha256_crypt",
bsdi_crypt__max_rounds = 30,
bsdi_crypt__default_rounds = 25,
bsdi_crypt__vary_rounds = 0,
sha256_crypt__max_rounds = 3000,
sha256_crypt__min_rounds = 2000,
sha256_crypt__default_rounds = 3000,
phpass__ident = "H",
phpass__default_rounds = 7,
)
def test_12_hash_needs_update(self):
"test hash_needs_update() method"
cc = CryptContext(**self.sample_policy_1)
# check deprecated scheme
self.assertTrue(cc.hash_needs_update('9XXD4trGYeGJA'))
self.assertFalse(cc.hash_needs_update('$1$J8HC2RCr$HcmM.7NxB2weSvlw2FgzU0'))
# check min rounds
self.assertTrue(cc.hash_needs_update('$5$rounds=1999$jD81UCoo.zI.UETs$Y7qSTQ6mTiU9qZB4fRr43wRgQq4V.5AAf7F97Pzxey/'))
self.assertFalse(cc.hash_needs_update('$5$rounds=2000$228SSRje04cnNCaQ$YGV4RYu.5sNiBvorQDlO0WWQjyJVGKBcJXz3OtyQ2u8'))
# check max rounds
self.assertFalse(cc.hash_needs_update('$5$rounds=3000$fS9iazEwTKi7QPW4$VasgBC8FqlOvD7x2HhABaMXCTh9jwHclPA9j5YQdns.'))
self.assertTrue(cc.hash_needs_update('$5$rounds=3001$QlFHHifXvpFX4PLs$/0ekt7lSs/lOikSerQ0M/1porEHxYq7W/2hdFpxA3fA'))
#===================================================================
# border cases
#===================================================================
def test_30_nonstring_hash(self):
"test non-string hash values cause error"
#
# test hash=None or some other non-string causes TypeError
# and that explicit-scheme code path behaves the same.
#
cc = CryptContext(["des_crypt"])
for hash, kwds in [
(None, {}),
(None, {"scheme": "des_crypt"}),
(1, {}),
((), {}),
]:
self.assertRaises(TypeError, cc.hash_needs_update, hash, **kwds)
cc2 = CryptContext(["mysql323"])
self.assertRaises(TypeError, cc2.hash_needs_update, None)
#===================================================================
# eoc
#===================================================================
#=============================================================================
# LazyCryptContext
#=============================================================================
class dummy_2(uh.StaticHandler):
name = "dummy_2"
class LazyCryptContextTest(TestCase):
descriptionPrefix = "LazyCryptContext"
def setUp(self):
TestCase.setUp(self)
# make sure this isn't registered before OR after
unload_handler_name("dummy_2")
self.addCleanup(unload_handler_name, "dummy_2")
# silence some warnings
warnings.filterwarnings("ignore",
r"CryptContext\(\)\.replace\(\) has been deprecated")
warnings.filterwarnings("ignore", ".*(CryptPolicy|context\.policy).*(has|have) been deprecated.*")
def test_kwd_constructor(self):
"test plain kwds"
self.assertFalse(has_crypt_handler("dummy_2"))
register_crypt_handler_path("dummy_2", "passlib.tests.test_context")
cc = LazyCryptContext(iter(["dummy_2", "des_crypt"]), deprecated=["des_crypt"])
self.assertFalse(has_crypt_handler("dummy_2", True))
self.assertTrue(cc.policy.handler_is_deprecated("des_crypt"))
self.assertEqual(cc.policy.schemes(), ["dummy_2", "des_crypt"])
self.assertTrue(has_crypt_handler("dummy_2", True))
def test_callable_constructor(self):
"test create_policy() hook, returning CryptPolicy"
self.assertFalse(has_crypt_handler("dummy_2"))
register_crypt_handler_path("dummy_2", "passlib.tests.test_context")
def create_policy(flag=False):
self.assertTrue(flag)
return CryptPolicy(schemes=iter(["dummy_2", "des_crypt"]), deprecated=["des_crypt"])
cc = LazyCryptContext(create_policy=create_policy, flag=True)
self.assertFalse(has_crypt_handler("dummy_2", True))
self.assertTrue(cc.policy.handler_is_deprecated("des_crypt"))
self.assertEqual(cc.policy.schemes(), ["dummy_2", "des_crypt"])
self.assertTrue(has_crypt_handler("dummy_2", True))
#=============================================================================
# eof
#=============================================================================

View File

@ -0,0 +1,976 @@
"""test passlib.ext.django"""
#=============================================================================
# imports
#=============================================================================
from __future__ import with_statement
# core
import logging; log = logging.getLogger(__name__)
import sys
import warnings
# site
# pkg
from passlib.apps import django10_context, django14_context, django16_context
from passlib.context import CryptContext
import passlib.exc as exc
from passlib.utils.compat import iteritems, unicode, get_method_function, u, PY3
from passlib.utils import memoized_property
from passlib.registry import get_crypt_handler
# tests
from passlib.tests.utils import TestCase, skipUnless, catch_warnings, TEST_MODE, has_active_backend
from passlib.tests.test_handlers import get_handler_case
# local
#=============================================================================
# configure django settings for testcases
#=============================================================================
from passlib.ext.django.utils import DJANGO_VERSION
# disable all Django integration tests under py3,
# since Django doesn't support py3 yet.
if PY3 and DJANGO_VERSION < (1,5):
DJANGO_VERSION = ()
# convert django version to some cheap flags
has_django = bool(DJANGO_VERSION)
has_django0 = has_django and DJANGO_VERSION < (1,0)
has_django1 = DJANGO_VERSION >= (1,0)
has_django14 = DJANGO_VERSION >= (1,4)
# import and configure empty django settings
if has_django:
from django.conf import settings, LazySettings
if not isinstance(settings, LazySettings):
# this probably means django globals have been configured already,
# which we don't want, since test cases reset and manipulate settings.
raise RuntimeError("expected django.conf.settings to be LazySettings: %r" % (settings,))
# else configure a blank settings instance for the unittests
if has_django0:
if settings._target is None:
from django.conf import UserSettingsHolder, global_settings
settings._target = UserSettingsHolder(global_settings)
elif not settings.configured:
settings.configure()
#=============================================================================
# support funcs
#=============================================================================
# flag for update_settings() to remove specified key entirely
UNSET = object()
def update_settings(**kwds):
"""helper to update django settings from kwds"""
for k,v in iteritems(kwds):
if v is UNSET:
if hasattr(settings, k):
if has_django0:
delattr(settings._target, k)
else:
delattr(settings, k)
else:
setattr(settings, k, v)
if has_django:
from django.contrib.auth.models import User
class FakeUser(User):
"mock user object for use in testing"
# NOTE: this mainly just overrides .save() to test commit behavior.
@memoized_property
def saved_passwords(self):
return []
def pop_saved_passwords(self):
try:
return self.saved_passwords[:]
finally:
del self.saved_passwords[:]
def save(self, update_fields=None):
# NOTE: ignoring update_fields for test purposes
self.saved_passwords.append(self.password)
def create_mock_setter():
state = []
def setter(password):
state.append(password)
def popstate():
try:
return state[:]
finally:
del state[:]
setter.popstate = popstate
return setter
#=============================================================================
# work up stock django config
#=============================================================================
sample_hashes = {} # override sample hashes used in test cases
if DJANGO_VERSION >= (1,6):
stock_config = django16_context.to_dict()
stock_config.update(
deprecated="auto",
django_pbkdf2_sha1__default_rounds=12000,
django_pbkdf2_sha256__default_rounds=12000,
)
sample_hashes.update(
django_pbkdf2_sha256=("not a password", "pbkdf2_sha256$12000$rpUPFQOVetrY$cEcWG4DjjDpLrDyXnduM+XJUz25U63RcM3//xaFnBnw="),
)
elif DJANGO_VERSION >= (1,4):
stock_config = django14_context.to_dict()
stock_config.update(
deprecated="auto",
django_pbkdf2_sha1__default_rounds=10000,
django_pbkdf2_sha256__default_rounds=10000,
)
elif DJANGO_VERSION >= (1,0):
stock_config = django10_context.to_dict()
else:
# 0.9.6 config
stock_config = dict(
schemes=["django_salted_sha1", "django_salted_md5", "hex_md5"],
deprecated=["hex_md5"]
)
#=============================================================================
# test utils
#=============================================================================
class _ExtensionSupport(object):
"support funcs for loading/unloading extension"
#===================================================================
# support funcs
#===================================================================
@classmethod
def _iter_patch_candidates(cls):
"""helper to scan for monkeypatches.
returns tuple containing:
* object (module or class)
* attribute of object
* value of attribute
* whether it should or should not be patched
"""
# XXX: this and assert_unpatched() could probably be refactored to use
# the PatchManager class to do the heavy lifting.
from django.contrib.auth import models
user_attrs = ["check_password", "set_password"]
model_attrs = ["check_password"]
objs = [(models, model_attrs), (models.User, user_attrs)]
if has_django14:
from django.contrib.auth import hashers
model_attrs.append("make_password")
objs.append((hashers, ["check_password", "make_password",
"get_hasher", "identify_hasher"]))
if has_django0:
user_attrs.extend(["has_usable_password", "set_unusable_password"])
for obj, patched in objs:
for attr in dir(obj):
if attr.startswith("_"):
continue
value = obj.__dict__.get(attr, UNSET) # can't use getattr() due to GAE
if value is UNSET and attr not in patched:
continue
value = get_method_function(value)
source = getattr(value, "__module__", None)
if source:
yield obj, attr, source, (attr in patched)
#===================================================================
# verify current patch state
#===================================================================
def assert_unpatched(self):
"test that django is in unpatched state"
# make sure we aren't currently patched
mod = sys.modules.get("passlib.ext.django.models")
self.assertFalse(mod and mod._patched, "patch should not be enabled")
# make sure no objects have been replaced, by checking __module__
for obj, attr, source, patched in self._iter_patch_candidates():
if patched:
self.assertTrue(source.startswith("django.contrib.auth."),
"obj=%r attr=%r was not reverted: %r" %
(obj, attr, source))
else:
self.assertFalse(source.startswith("passlib."),
"obj=%r attr=%r should not have been patched: %r" %
(obj, attr, source))
def assert_patched(self, context=None):
"helper to ensure django HAS been patched, and is using specified config"
# make sure we're currently patched
mod = sys.modules.get("passlib.ext.django.models")
self.assertTrue(mod and mod._patched, "patch should have been enabled")
# make sure only the expected objects have been patched
for obj, attr, source, patched in self._iter_patch_candidates():
if patched:
self.assertTrue(source == "passlib.ext.django.models",
"obj=%r attr=%r should have been patched: %r" %
(obj, attr, source))
else:
self.assertFalse(source.startswith("passlib."),
"obj=%r attr=%r should not have been patched: %r" %
(obj, attr, source))
# check context matches
if context is not None:
context = CryptContext._norm_source(context)
self.assertEqual(mod.password_context.to_dict(resolve=True),
context.to_dict(resolve=True))
#===================================================================
# load / unload the extension (and verify it worked)
#===================================================================
_config_keys = ["PASSLIB_CONFIG", "PASSLIB_CONTEXT", "PASSLIB_GET_CATEGORY"]
def load_extension(self, check=True, **kwds):
"helper to load extension with specified config & patch django"
self.unload_extension()
if check:
config = kwds.get("PASSLIB_CONFIG") or kwds.get("PASSLIB_CONTEXT")
for key in self._config_keys:
kwds.setdefault(key, UNSET)
update_settings(**kwds)
import passlib.ext.django.models
if check:
self.assert_patched(context=config)
def unload_extension(self):
"helper to remove patches and unload extension"
# remove patches and unload module
mod = sys.modules.get("passlib.ext.django.models")
if mod:
mod._remove_patch()
del sys.modules["passlib.ext.django.models"]
# wipe config from django settings
update_settings(**dict((key, UNSET) for key in self._config_keys))
# check everything's gone
self.assert_unpatched()
#===================================================================
# eoc
#===================================================================
# XXX: rename to ExtensionFixture?
class _ExtensionTest(TestCase, _ExtensionSupport):
def setUp(self):
super(_ExtensionTest, self).setUp()
self.require_TEST_MODE("default")
if not has_django:
raise self.skipTest("Django not installed")
# reset to baseline, and verify it worked
self.unload_extension()
# and do the same when the test exits
self.addCleanup(self.unload_extension)
#=============================================================================
# extension tests
#=============================================================================
class DjangoBehaviorTest(_ExtensionTest):
"tests model to verify it matches django's behavior"
descriptionPrefix = "verify django behavior"
patched = False
config = stock_config
# NOTE: if this test fails, it means we're not accounting for
# some part of django's hashing logic, or that this is
# running against an untested version of django with a new
# hashing policy.
@property
def context(self):
return CryptContext._norm_source(self.config)
def assert_unusable_password(self, user):
"""check that user object is set to 'unusable password' constant"""
if DJANGO_VERSION >= (1,6):
# 1.6 on adds a random(?) suffix
self.assertTrue(user.password.startswith("!"))
else:
self.assertEqual(user.password, "!")
if has_django1 or self.patched:
self.assertFalse(user.has_usable_password())
self.assertEqual(user.pop_saved_passwords(), [])
def assert_valid_password(self, user, hash=UNSET, saved=None):
"""check that user object has a usuable password hash.
:param hash: optionally check it has this exact hash
:param saved: check that mock commit history
for user.password matches this list
"""
if hash is UNSET:
self.assertNotEqual(user.password, "!")
self.assertNotEqual(user.password, None)
else:
self.assertEqual(user.password, hash)
if has_django1 or self.patched:
self.assertTrue(user.has_usable_password())
self.assertEqual(user.pop_saved_passwords(),
[] if saved is None else [saved])
def test_config(self):
"""test hashing interface
this function is run against both the actual django code, to
verify the assumptions of the unittests are correct;
and run against the passlib extension, to verify it matches
those assumptions.
"""
patched, config = self.patched, self.config
# this tests the following methods:
# User.set_password()
# User.check_password()
# make_password() -- 1.4 only
# check_password()
# identify_hasher()
# User.has_usable_password()
# User.set_unusable_password()
# XXX: this take a while to run. what could be trimmed?
# TODO: get_hasher()
#=======================================================
# setup helpers & imports
#=======================================================
ctx = self.context
setter = create_mock_setter()
PASS1 = "toomanysecrets"
WRONG1 = "letmein"
has_hashers = False
has_identify_hasher = False
if has_django14:
from passlib.ext.django.utils import hasher_to_passlib_name, passlib_to_hasher_name
from django.contrib.auth.hashers import check_password, make_password, is_password_usable
if patched or DJANGO_VERSION > (1,5):
# identify_hasher()
# django 1.4 -- not present
# django 1.5 -- present (added in django ticket 18184)
# passlib integration -- present even under 1.4
from django.contrib.auth.hashers import identify_hasher
has_identify_hasher = True
hash_hashers = True
else:
from django.contrib.auth.models import check_password
#=======================================================
# make sure extension is configured correctly
#=======================================================
if patched:
# contexts should match
from passlib.ext.django.models import password_context
self.assertEqual(password_context.to_dict(resolve=True),
ctx.to_dict(resolve=True))
# should have patched both places
if has_django14:
from django.contrib.auth.models import check_password as check_password2
self.assertIs(check_password2, check_password)
#=======================================================
# default algorithm
#=======================================================
# User.set_password() should use default alg
user = FakeUser()
user.set_password(PASS1)
self.assertTrue(ctx.handler().verify(PASS1, user.password))
self.assert_valid_password(user)
# User.check_password() - n/a
# make_password() should use default alg
if has_django14:
hash = make_password(PASS1)
self.assertTrue(ctx.handler().verify(PASS1, hash))
# check_password() - n/a
#=======================================================
# empty password behavior
#=======================================================
if (1,4) <= DJANGO_VERSION < (1,6):
# NOTE: django 1.4-1.5 treat empty password as invalid
# User.set_password() should set unusable flag
user = FakeUser()
user.set_password('')
self.assert_unusable_password(user)
# User.check_password() should never return True
user = FakeUser()
user.password = hash = ctx.encrypt("")
self.assertFalse(user.check_password(""))
self.assert_valid_password(user, hash)
# make_password() should reject empty passwords
self.assertEqual(make_password(""), "!")
# check_password() should never return True
self.assertFalse(check_password("", hash))
else:
# User.set_password() should use default alg
user = FakeUser()
user.set_password('')
hash = user.password
self.assertTrue(ctx.handler().verify('', hash))
self.assert_valid_password(user, hash)
# User.check_password() should return True
self.assertTrue(user.check_password(""))
self.assert_valid_password(user, hash)
# no make_password()
# check_password() should return True
self.assertTrue(check_password("", hash))
#=======================================================
# 'unusable flag' behavior
#=======================================================
if has_django1 or patched:
# sanity check via user.set_unusable_password()
user = FakeUser()
user.set_unusable_password()
self.assert_unusable_password(user)
# ensure User.set_password() sets unusable flag
user = FakeUser()
user.set_password(None)
if DJANGO_VERSION < (1,2):
# would set password to hash of "None"
self.assert_valid_password(user)
else:
self.assert_unusable_password(user)
# User.check_password() should always fail
if DJANGO_VERSION < (1,2):
self.assertTrue(user.check_password(None))
self.assertTrue(user.check_password('None'))
self.assertFalse(user.check_password(''))
self.assertFalse(user.check_password(PASS1))
self.assertFalse(user.check_password(WRONG1))
else:
self.assertFalse(user.check_password(None))
self.assertFalse(user.check_password('None'))
self.assertFalse(user.check_password(''))
self.assertFalse(user.check_password(PASS1))
self.assertFalse(user.check_password(WRONG1))
self.assert_unusable_password(user)
# make_password() should also set flag
if has_django14:
if DJANGO_VERSION >= (1,6):
self.assertTrue(make_password(None).startswith("!"))
else:
self.assertEqual(make_password(None), "!")
# check_password() should return False (didn't handle disabled under 1.3)
if has_django14 or patched:
self.assertFalse(check_password(PASS1, '!'))
# identify_hasher() and is_password_usable() should reject it
if has_django14:
self.assertFalse(is_password_usable(user.password))
if has_identify_hasher:
self.assertRaises(ValueError, identify_hasher, user.password)
#=======================================================
# hash=None
#=======================================================
# User.set_password() - n/a
# User.check_password() - returns False
user = FakeUser()
user.password = None
if has_django14 or patched:
self.assertFalse(user.check_password(PASS1))
else:
self.assertRaises(TypeError, user.check_password, PASS1)
if has_django1 or patched:
if DJANGO_VERSION < (1,2):
self.assertTrue(user.has_usable_password())
else:
self.assertFalse(user.has_usable_password())
# make_password() - n/a
# check_password() - error
if has_django14 or patched:
self.assertFalse(check_password(PASS1, None))
else:
self.assertRaises(AttributeError, check_password, PASS1, None)
# identify_hasher() - error
if has_identify_hasher:
self.assertRaises(TypeError, identify_hasher, None)
#=======================================================
# empty & invalid hash values
# NOTE: django 1.5 behavior change due to django ticket 18453
# NOTE: passlib integration tries to match current django version
#=======================================================
for hash in ("", # empty hash
"$789$foo", # empty identifier
):
# User.set_password() - n/a
# User.check_password()
# empty
# -----
# django 1.3 and earlier -- blank hash returns False
# django 1.4 -- blank threw error (fixed in 1.5)
# django 1.5 -- blank hash returns False
#
# invalid
# -------
# django 1.4 and earlier -- invalid hash threw error (fixed in 1.5)
# django 1.5 -- invalid hash returns False
user = FakeUser()
user.password = hash
if DJANGO_VERSION >= (1,5) or (not hash and DJANGO_VERSION < (1,4)):
# returns False for hash
self.assertFalse(user.check_password(PASS1))
else:
# throws error for hash
self.assertRaises(ValueError, user.check_password, PASS1)
# verify hash wasn't changed/upgraded during check_password() call
self.assertEqual(user.password, hash)
self.assertEqual(user.pop_saved_passwords(), [])
# User.has_usable_password()
# passlib shim for django 0.x -- invalid/empty usable, to match 1.0-1.4
# django 1.0-1.4 -- invalid/empty usable (fixed in 1.5)
# django 1.5 -- invalid/empty no longer usable
if has_django1 or self.patched:
if DJANGO_VERSION < (1,5):
self.assertTrue(user.has_usable_password())
else:
self.assertFalse(user.has_usable_password())
# make_password() - n/a
# check_password()
# django 1.4 and earlier -- invalid/empty hash threw error (fixed in 1.5)
# django 1.5 -- invalid/empty hash now returns False
if DJANGO_VERSION < (1,5):
self.assertRaises(ValueError, check_password, PASS1, hash)
else:
self.assertFalse(check_password(PASS1, hash))
# identify_hasher() - throws error
if has_identify_hasher:
self.assertRaises(ValueError, identify_hasher, hash)
#=======================================================
# run through all the schemes in the context,
# testing various bits of per-scheme behavior.
#=======================================================
for scheme in ctx.schemes():
#-------------------------------------------------------
# setup constants & imports, pick a sample secret/hash combo
#-------------------------------------------------------
handler = ctx.handler(scheme)
deprecated = ctx._is_deprecated_scheme(scheme)
assert not deprecated or scheme != ctx.default_scheme()
try:
testcase = get_handler_case(scheme)
except exc.MissingBackendError:
assert scheme == "bcrypt"
continue
assert testcase.handler is handler
if testcase.is_disabled_handler:
continue
if not has_active_backend(handler):
assert scheme == "django_bcrypt"
continue
try:
secret, hash = sample_hashes[scheme]
except KeyError:
while True:
secret, hash = testcase('setUp').get_sample_hash()
if secret: # don't select blank passwords, especially under django 1.4/1.5
break
other = 'dontletmein'
# User.set_password() - n/a
#-------------------------------------------------------
# User.check_password()+migration against known hash
#-------------------------------------------------------
user = FakeUser()
user.password = hash
# check against invalid password
if has_django1 or patched:
self.assertFalse(user.check_password(None))
else:
self.assertRaises(TypeError, user.check_password, None)
##self.assertFalse(user.check_password(''))
self.assertFalse(user.check_password(other))
self.assert_valid_password(user, hash)
# check against valid password
if has_django0 and isinstance(secret, unicode):
secret = secret.encode("utf-8")
self.assertTrue(user.check_password(secret))
# check if it upgraded the hash
# NOTE: needs_update kept separate in case we need to test rounds.
needs_update = deprecated
if needs_update:
self.assertNotEqual(user.password, hash)
self.assertFalse(handler.identify(user.password))
self.assertTrue(ctx.handler().verify(secret, user.password))
self.assert_valid_password(user, saved=user.password)
else:
self.assert_valid_password(user, hash)
# don't need to check rest for most deployments
if TEST_MODE(max="default"):
continue
#-------------------------------------------------------
# make_password() correctly selects algorithm
#-------------------------------------------------------
if has_django14:
hash2 = make_password(secret, hasher=passlib_to_hasher_name(scheme))
self.assertTrue(handler.verify(secret, hash2))
#-------------------------------------------------------
# check_password()+setter against known hash
#-------------------------------------------------------
if has_django14 or patched:
# should call setter only if it needs_update
self.assertTrue(check_password(secret, hash, setter=setter))
self.assertEqual(setter.popstate(), [secret] if needs_update else [])
# should not call setter
self.assertFalse(check_password(other, hash, setter=setter))
self.assertEqual(setter.popstate(), [])
### check preferred kwd is ignored (django 1.4 feature we don't support)
##self.assertTrue(check_password(secret, hash, setter=setter, preferred='fooey'))
##self.assertEqual(setter.popstate(), [secret])
elif patched or scheme != "hex_md5":
# django 1.3 never called check_password() for hex_md5
self.assertTrue(check_password(secret, hash))
self.assertFalse(check_password(other, hash))
# TODO: get_hasher()
#-------------------------------------------------------
# identify_hasher() recognizes known hash
#-------------------------------------------------------
if has_identify_hasher:
self.assertTrue(is_password_usable(hash))
name = hasher_to_passlib_name(identify_hasher(hash).algorithm)
self.assertEqual(name, scheme)
class ExtensionBehaviorTest(DjangoBehaviorTest):
"test model to verify passlib.ext.django conforms to it"
descriptionPrefix = "verify extension behavior"
patched = True
config = dict(
schemes="sha256_crypt,md5_crypt,des_crypt",
deprecated="des_crypt",
)
def setUp(self):
super(ExtensionBehaviorTest, self).setUp()
self.load_extension(PASSLIB_CONFIG=self.config)
class DjangoExtensionTest(_ExtensionTest):
"""test the ``passlib.ext.django`` plugin"""
descriptionPrefix = "passlib.ext.django plugin"
#===================================================================
# monkeypatch testing
#===================================================================
def test_00_patch_control(self):
"test set_django_password_context patch/unpatch"
# check config="disabled"
self.load_extension(PASSLIB_CONFIG="disabled", check=False)
self.assert_unpatched()
# check legacy config=None
with self.assertWarningList("PASSLIB_CONFIG=None is deprecated"):
self.load_extension(PASSLIB_CONFIG=None, check=False)
self.assert_unpatched()
# try stock django 1.0 context
self.load_extension(PASSLIB_CONFIG="django-1.0", check=False)
self.assert_patched(context=django10_context)
# try to remove patch
self.unload_extension()
# patch to use stock django 1.4 context
self.load_extension(PASSLIB_CONFIG="django-1.4", check=False)
self.assert_patched(context=django14_context)
# try to remove patch again
self.unload_extension()
def test_01_overwrite_detection(self):
"test detection of foreign monkeypatching"
# NOTE: this sets things up, and spot checks two methods,
# this should be enough to verify patch manager is working.
# TODO: test unpatch behavior honors flag.
# configure plugin to use sample context
config = "[passlib]\nschemes=des_crypt\n"
self.load_extension(PASSLIB_CONFIG=config)
# setup helpers
import django.contrib.auth.models as models
from passlib.ext.django.models import _manager
def dummy():
pass
# mess with User.set_password, make sure it's detected
orig = models.User.set_password
models.User.set_password = dummy
with self.assertWarningList("another library has patched.*User\.set_password"):
_manager.check_all()
models.User.set_password = orig
# mess with models.check_password, make sure it's detected
orig = models.check_password
models.check_password = dummy
with self.assertWarningList("another library has patched.*models:check_password"):
_manager.check_all()
models.check_password = orig
def test_02_handler_wrapper(self):
"test Hasher-compatible handler wrappers"
if not has_django14:
raise self.skipTest("Django >= 1.4 not installed")
from passlib.ext.django.utils import get_passlib_hasher
from django.contrib.auth import hashers
# should return native django hasher if available
hasher = get_passlib_hasher("hex_md5")
self.assertIsInstance(hasher, hashers.UnsaltedMD5PasswordHasher)
hasher = get_passlib_hasher("django_bcrypt")
self.assertIsInstance(hasher, hashers.BCryptPasswordHasher)
# otherwise should return wrapper
from passlib.hash import sha256_crypt
hasher = get_passlib_hasher("sha256_crypt")
self.assertEqual(hasher.algorithm, "passlib_sha256_crypt")
# and wrapper should return correct hash
encoded = hasher.encode("stub")
self.assertTrue(sha256_crypt.verify("stub", encoded))
self.assertTrue(hasher.verify("stub", encoded))
self.assertFalse(hasher.verify("xxxx", encoded))
# test wrapper accepts options
encoded = hasher.encode("stub", "abcd"*4, iterations=1234)
self.assertEqual(encoded, "$5$rounds=1234$abcdabcdabcdabcd$"
"v2RWkZQzctPdejyRqmmTDQpZN6wTh7.RUy9zF2LftT6")
self.assertEqual(hasher.safe_summary(encoded),
{'algorithm': 'sha256_crypt',
'salt': u('abcdab**********'),
'iterations': 1234,
'hash': u('v2RWkZ*************************************'),
})
#===================================================================
# PASSLIB_CONFIG settings
#===================================================================
def test_11_config_disabled(self):
"test PASSLIB_CONFIG='disabled'"
# test config=None (deprecated)
with self.assertWarningList("PASSLIB_CONFIG=None is deprecated"):
self.load_extension(PASSLIB_CONFIG=None, check=False)
self.assert_unpatched()
# test disabled config
self.load_extension(PASSLIB_CONFIG="disabled", check=False)
self.assert_unpatched()
def test_12_config_presets(self):
"test PASSLIB_CONFIG='<preset>'"
# test django presets
self.load_extension(PASSLIB_CONTEXT="django-default", check=False)
if DJANGO_VERSION >= (1,6):
ctx = django16_context
elif DJANGO_VERSION >= (1,4):
ctx = django14_context
else:
ctx = django10_context
self.assert_patched(ctx)
self.load_extension(PASSLIB_CONFIG="django-1.0", check=False)
self.assert_patched(django10_context)
self.load_extension(PASSLIB_CONFIG="django-1.4", check=False)
self.assert_patched(django14_context)
def test_13_config_defaults(self):
"test PASSLIB_CONFIG default behavior"
# check implicit default
from passlib.ext.django.utils import PASSLIB_DEFAULT
default = CryptContext.from_string(PASSLIB_DEFAULT)
self.load_extension()
self.assert_patched(PASSLIB_DEFAULT)
# check default preset
self.load_extension(PASSLIB_CONTEXT="passlib-default", check=False)
self.assert_patched(PASSLIB_DEFAULT)
# check explicit string
self.load_extension(PASSLIB_CONTEXT=PASSLIB_DEFAULT, check=False)
self.assert_patched(PASSLIB_DEFAULT)
def test_14_config_invalid(self):
"test PASSLIB_CONFIG type checks"
update_settings(PASSLIB_CONTEXT=123, PASSLIB_CONFIG=UNSET)
self.assertRaises(TypeError, __import__, 'passlib.ext.django.models')
self.unload_extension()
update_settings(PASSLIB_CONFIG="missing-preset", PASSLIB_CONTEXT=UNSET)
self.assertRaises(ValueError, __import__, 'passlib.ext.django.models')
#===================================================================
# PASSLIB_GET_CATEGORY setting
#===================================================================
def test_21_category_setting(self):
"test PASSLIB_GET_CATEGORY parameter"
# define config where rounds can be used to detect category
config = dict(
schemes = ["sha256_crypt"],
sha256_crypt__default_rounds = 1000,
staff__sha256_crypt__default_rounds = 2000,
superuser__sha256_crypt__default_rounds = 3000,
)
from passlib.hash import sha256_crypt
def run(**kwds):
"helper to take in user opts, return rounds used in password"
user = FakeUser(**kwds)
user.set_password("stub")
return sha256_crypt.from_string(user.password).rounds
# test default get_category
self.load_extension(PASSLIB_CONFIG=config)
self.assertEqual(run(), 1000)
self.assertEqual(run(is_staff=True), 2000)
self.assertEqual(run(is_superuser=True), 3000)
# test patch uses explicit get_category function
def get_category(user):
return user.first_name or None
self.load_extension(PASSLIB_CONTEXT=config,
PASSLIB_GET_CATEGORY=get_category)
self.assertEqual(run(), 1000)
self.assertEqual(run(first_name='other'), 1000)
self.assertEqual(run(first_name='staff'), 2000)
self.assertEqual(run(first_name='superuser'), 3000)
# test patch can disable get_category entirely
def get_category(user):
return None
self.load_extension(PASSLIB_CONTEXT=config,
PASSLIB_GET_CATEGORY=get_category)
self.assertEqual(run(), 1000)
self.assertEqual(run(first_name='other'), 1000)
self.assertEqual(run(first_name='staff', is_staff=True), 1000)
self.assertEqual(run(first_name='superuser', is_superuser=True), 1000)
# test bad value
self.assertRaises(TypeError, self.load_extension, PASSLIB_CONTEXT=config,
PASSLIB_GET_CATEGORY='x')
#===================================================================
# eoc
#===================================================================
from passlib.context import CryptContext
class ContextWithHook(CryptContext):
"""subclass which invokes update_hook(self) before major actions"""
@staticmethod
def update_hook(self):
pass
def encrypt(self, *args, **kwds):
self.update_hook(self)
return super(ContextWithHook, self).encrypt(*args, **kwds)
def verify(self, *args, **kwds):
self.update_hook(self)
return super(ContextWithHook, self).verify(*args, **kwds)
# hack up the some of the real django tests to run w/ extension loaded,
# to ensure we mimic their behavior.
if has_django14:
from passlib.tests.utils import patchAttr
if DJANGO_VERSION >= (1,6):
from django.contrib.auth.tests import test_hashers as _thmod
else:
from django.contrib.auth.tests import hashers as _thmod
class HashersTest(_thmod.TestUtilsHashPass, _ExtensionSupport):
"""run django's hasher unittests against passlib's extension
and workalike implementations"""
def setUp(self):
# NOTE: omitted orig setup, want to install our extension,
# and load hashers through it instead.
self.load_extension(PASSLIB_CONTEXT=stock_config, check=False)
from passlib.ext.django.models import password_context
# update test module to use our versions of some hasher funcs
from django.contrib.auth import hashers
for attr in ["make_password",
"check_password",
"identify_hasher",
"get_hasher"]:
patchAttr(self, _thmod, attr, getattr(hashers, attr))
# django 1.5 tests expect empty django_des_crypt salt field
if DJANGO_VERSION > (1,4):
from passlib.hash import django_des_crypt
patchAttr(self, django_des_crypt, "use_duplicate_salt", False)
# hack: need password_context to keep up to date with hasher.iterations
if DJANGO_VERSION >= (1,6):
def update_hook(self):
rounds = _thmod.get_hasher("pbkdf2_sha256").iterations
self.update(
django_pbkdf2_sha256__min_rounds=rounds,
django_pbkdf2_sha256__default_rounds=rounds,
django_pbkdf2_sha256__max_rounds=rounds,
)
patchAttr(self, password_context, "__class__", ContextWithHook)
patchAttr(self, password_context, "update_hook", update_hook)
# omitting this test, since it depends on updated to django hasher settings
test_pbkdf2_upgrade_new_hasher = lambda self: self.skipTest("omitted by passlib")
def tearDown(self):
self.unload_extension()
super(HashersTest, self).tearDown()
HashersTest = skipUnless(TEST_MODE("default"),
"requires >= 'default' test mode")(HashersTest)
#=============================================================================
# eof
#=============================================================================

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,472 @@
"""passlib.tests.test_handlers_bcrypt - tests for passlib hash algorithms"""
#=============================================================================
# imports
#=============================================================================
from __future__ import with_statement
# core
import hashlib
import logging; log = logging.getLogger(__name__)
import os
import sys
import warnings
# site
# pkg
from passlib import hash
from passlib.utils import repeat_string
from passlib.utils.compat import irange, PY3, u, get_method_function
from passlib.tests.utils import TestCase, HandlerCase, skipUnless, \
TEST_MODE, b, catch_warnings, UserHandlerMixin, randintgauss, EncodingHandlerMixin
from passlib.tests.test_handlers import UPASS_WAV, UPASS_USD, UPASS_TABLE
# module
#=============================================================================
# bcrypt
#=============================================================================
class _bcrypt_test(HandlerCase):
"base for BCrypt test cases"
handler = hash.bcrypt
secret_size = 72
reduce_default_rounds = True
fuzz_salts_need_bcrypt_repair = True
known_correct_hashes = [
#
# from JTR 1.7.9
#
('U*U*U*U*', '$2a$05$c92SVSfjeiCD6F2nAD6y0uBpJDjdRkt0EgeC4/31Rf2LUZbDRDE.O'),
('U*U***U', '$2a$05$WY62Xk2TXZ7EvVDQ5fmjNu7b0GEzSzUXUh2cllxJwhtOeMtWV3Ujq'),
('U*U***U*', '$2a$05$Fa0iKV3E2SYVUlMknirWU.CFYGvJ67UwVKI1E2FP6XeLiZGcH3MJi'),
('*U*U*U*U', '$2a$05$.WRrXibc1zPgIdRXYfv.4uu6TD1KWf0VnHzq/0imhUhuxSxCyeBs2'),
('', '$2a$05$Otz9agnajgrAe0.kFVF9V.tzaStZ2s1s4ZWi/LY4sw2k/MTVFj/IO'),
#
# test vectors from http://www.openwall.com/crypt v1.2
# note that this omits any hashes that depend on crypt_blowfish's
# various CVE-2011-2483 workarounds (hash 2a and \xff\xff in password,
# and any 2x hashes); and only contain hashes which are correct
# under both crypt_blowfish 1.2 AND OpenBSD.
#
('U*U', '$2a$05$CCCCCCCCCCCCCCCCCCCCC.E5YPO9kmyuRGyh0XouQYb4YMJKvyOeW'),
('U*U*', '$2a$05$CCCCCCCCCCCCCCCCCCCCC.VGOzA784oUp/Z0DY336zx7pLYAy0lwK'),
('U*U*U', '$2a$05$XXXXXXXXXXXXXXXXXXXXXOAcXxm9kjPGEMsLznoKqmqw7tc8WCx4a'),
('', '$2a$05$CCCCCCCCCCCCCCCCCCCCC.7uG0VCzI2bS7j6ymqJi9CdcdxiRTWNy'),
('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
'0123456789chars after 72 are ignored',
'$2a$05$abcdefghijklmnopqrstuu5s2v8.iXieOjg/.AySBTTZIIVFJeBui'),
(b('\xa3'),
'$2a$05$/OK.fbVrR/bpIqNJ5ianF.Sa7shbm4.OzKpvFnX1pQLmQW96oUlCq'),
(b('\xff\xa3345'),
'$2a$05$/OK.fbVrR/bpIqNJ5ianF.nRht2l/HRhr6zmCp9vYUvvsqynflf9e'),
(b('\xa3ab'),
'$2a$05$/OK.fbVrR/bpIqNJ5ianF.6IflQkJytoRVc1yuaNtHfiuq.FRlSIS'),
(b('\xaa')*72 + b('chars after 72 are ignored as usual'),
'$2a$05$/OK.fbVrR/bpIqNJ5ianF.swQOIzjOiJ9GHEPuhEkvqrUyvWhEMx6'),
(b('\xaa\x55'*36),
'$2a$05$/OK.fbVrR/bpIqNJ5ianF.R9xrDjiycxMbQE2bp.vgqlYpW5wx2yy'),
(b('\x55\xaa\xff'*24),
'$2a$05$/OK.fbVrR/bpIqNJ5ianF.9tQZzcJfm3uj2NvJ/n5xkhpqLrMpWCe'),
# keeping one of their 2y tests, because we are supporting that.
(b('\xa3'),
'$2y$05$/OK.fbVrR/bpIqNJ5ianF.Sa7shbm4.OzKpvFnX1pQLmQW96oUlCq'),
#
# from py-bcrypt tests
#
('', '$2a$06$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s.'),
('a', '$2a$10$k87L/MF28Q673VKh8/cPi.SUl7MU/rWuSiIDDFayrKk/1tBsSQu4u'),
('abc', '$2a$10$WvvTPHKwdBJ3uk0Z37EMR.hLA2W6N9AEBhEgrAOljy2Ae5MtaSIUi'),
('abcdefghijklmnopqrstuvwxyz',
'$2a$10$fVH8e28OQRj9tqiDXs1e1uxpsjN0c7II7YPKXua2NAKYvM6iQk7dq'),
('~!@#$%^&*() ~!@#$%^&*()PNBFRD',
'$2a$10$LgfYWkbzEvQ4JakH7rOvHe0y8pHKF9OaFgwUZ2q7W2FFZmZzJYlfS'),
#
# custom test vectors
#
# ensures utf-8 used for unicode
(UPASS_TABLE,
'$2a$05$Z17AXnnlpzddNUvnC6cZNOSwMA/8oNiKnHTHTwLlBijfucQQlHjaG'),
]
if TEST_MODE("full"):
#
# add some extra tests related to 2/2a
#
CONFIG_2 = '$2$05$' + '.'*22
CONFIG_A = '$2a$05$' + '.'*22
known_correct_hashes.extend([
("", CONFIG_2 + 'J2ihDv8vVf7QZ9BsaRrKyqs2tkn55Yq'),
("", CONFIG_A + 'J2ihDv8vVf7QZ9BsaRrKyqs2tkn55Yq'),
("abc", CONFIG_2 + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'),
("abc", CONFIG_A + 'ev6gDwpVye3oMCUpLY85aTpfBNHD0Ga'),
("abc"*23, CONFIG_2 + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'),
("abc"*23, CONFIG_A + '2kIdfSj/4/R/Q6n847VTvc68BXiRYZC'),
("abc"*24, CONFIG_2 + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'),
("abc"*24, CONFIG_A + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'),
("abc"*24+'x', CONFIG_2 + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'),
("abc"*24+'x', CONFIG_A + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'),
])
known_correct_configs = [
('$2a$04$uM6csdM8R9SXTex/gbTaye', UPASS_TABLE,
'$2a$04$uM6csdM8R9SXTex/gbTayezuvzFEufYGd2uB6of7qScLjQ4GwcD4G'),
]
known_unidentified_hashes = [
# invalid minor version
"$2b$12$EXRkfkdmXnagzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q",
"$2`$12$EXRkfkdmXnagzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q",
]
known_malformed_hashes = [
# bad char in otherwise correct hash
# \/
"$2a$12$EXRkfkdmXn!gzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q",
# unsupported (but recognized) minor version
"$2x$12$EXRkfkdmXnagzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q",
# rounds not zero-padded (py-bcrypt rejects this, therefore so do we)
'$2a$6$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s.'
# NOTE: salts with padding bits set are technically malformed,
# but we can reliably correct & issue a warning for that.
]
platform_crypt_support = [
("freedbsd|openbsd|netbsd", True),
("darwin", False),
# linux - may be present via addon, e.g. debian's libpam-unix2
# solaris - depends on policy
]
#===================================================================
# override some methods
#===================================================================
def setUp(self):
# ensure builtin is enabled for duration of test.
if TEST_MODE("full") and self.backend == "builtin":
key = "PASSLIB_BUILTIN_BCRYPT"
orig = os.environ.get(key)
if orig:
self.addCleanup(os.environ.__setitem__, key, orig)
else:
self.addCleanup(os.environ.__delitem__, key)
os.environ[key] = "enabled"
super(_bcrypt_test, self).setUp()
def populate_settings(self, kwds):
# builtin is still just way too slow.
if self.backend == "builtin":
kwds.setdefault("rounds", 4)
super(_bcrypt_test, self).populate_settings(kwds)
#===================================================================
# fuzz testing
#===================================================================
def os_supports_ident(self, hash):
"check if OS crypt is expected to support given ident"
if hash is None:
return True
# most OSes won't support 2x/2y
# XXX: definitely not the BSDs, but what about the linux variants?
from passlib.handlers.bcrypt import IDENT_2X, IDENT_2Y
if hash.startswith(IDENT_2X) or hash.startswith(IDENT_2Y):
return False
return True
def fuzz_verifier_bcrypt(self):
# test against bcrypt, if available
from passlib.handlers.bcrypt import IDENT_2, IDENT_2A, IDENT_2X, IDENT_2Y
from passlib.utils import to_native_str, to_bytes
try:
import bcrypt
except ImportError:
return
if not hasattr(bcrypt, "_ffi"):
return
def check_bcrypt(secret, hash):
"bcrypt"
secret = to_bytes(secret, self.fuzz_password_encoding)
#if hash.startswith(IDENT_2Y):
# hash = IDENT_2A + hash[4:]
if hash.startswith(IDENT_2):
# bcryptor doesn't support $2$ hashes; but we can fake it
# using the $2a$ algorithm, by repeating the password until
# it's 72 chars in length.
hash = IDENT_2A + hash[3:]
if secret:
secret = repeat_string(secret, 72)
hash = to_bytes(hash)
try:
return bcrypt.hashpw(secret, hash) == hash
except ValueError:
raise ValueError("bcrypt rejected hash: %r" % (hash,))
return check_bcrypt
def fuzz_verifier_pybcrypt(self):
# test against py-bcrypt, if available
from passlib.handlers.bcrypt import IDENT_2, IDENT_2A, IDENT_2X, IDENT_2Y
from passlib.utils import to_native_str
try:
import bcrypt
except ImportError:
return
if hasattr(bcrypt, "_ffi"):
return
def check_pybcrypt(secret, hash):
"pybcrypt"
secret = to_native_str(secret, self.fuzz_password_encoding)
if hash.startswith(IDENT_2Y):
hash = IDENT_2A + hash[4:]
try:
return bcrypt.hashpw(secret, hash) == hash
except ValueError:
raise ValueError("py-bcrypt rejected hash: %r" % (hash,))
return check_pybcrypt
def fuzz_verifier_bcryptor(self):
# test against bcryptor, if available
from passlib.handlers.bcrypt import IDENT_2, IDENT_2A, IDENT_2Y
from passlib.utils import to_native_str
try:
from bcryptor.engine import Engine
except ImportError:
return
def check_bcryptor(secret, hash):
"bcryptor"
secret = to_native_str(secret, self.fuzz_password_encoding)
if hash.startswith(IDENT_2Y):
hash = IDENT_2A + hash[4:]
elif hash.startswith(IDENT_2):
# bcryptor doesn't support $2$ hashes; but we can fake it
# using the $2a$ algorithm, by repeating the password until
# it's 72 chars in length.
hash = IDENT_2A + hash[3:]
if secret:
secret = repeat_string(secret, 72)
return Engine(False).hash_key(secret, hash) == hash
return check_bcryptor
def get_fuzz_settings(self):
secret, other, kwds = super(_bcrypt_test,self).get_fuzz_settings()
from passlib.handlers.bcrypt import IDENT_2, IDENT_2X
from passlib.utils import to_bytes
ident = kwds.get('ident')
if ident == IDENT_2X:
# 2x is just recognized, not supported. don't test with it.
del kwds['ident']
elif ident == IDENT_2 and other and repeat_string(to_bytes(other), len(to_bytes(secret))) == to_bytes(secret):
# avoid false failure due to flaw in 0-revision bcrypt:
# repeated strings like 'abc' and 'abcabc' hash identically.
other = self.get_fuzz_password()
return secret, other, kwds
def fuzz_setting_rounds(self):
# decrease default rounds for fuzz testing to speed up volume.
return randintgauss(5, 8, 6, 1)
#===================================================================
# custom tests
#===================================================================
known_incorrect_padding = [
# password, bad hash, good hash
# 2 bits of salt padding set
# ("loppux", # \/
# "$2a$12$oaQbBqq8JnSM1NHRPQGXORm4GCUMqp7meTnkft4zgSnrbhoKdDV0C",
# "$2a$12$oaQbBqq8JnSM1NHRPQGXOOm4GCUMqp7meTnkft4zgSnrbhoKdDV0C"),
("test", # \/
'$2a$04$oaQbBqq8JnSM1NHRPQGXORY4Vw3bdHKLIXTecPDRAcJ98cz1ilveO',
'$2a$04$oaQbBqq8JnSM1NHRPQGXOOY4Vw3bdHKLIXTecPDRAcJ98cz1ilveO'),
# all 4 bits of salt padding set
# ("Passlib11", # \/
# "$2a$12$M8mKpW9a2vZ7PYhq/8eJVcUtKxpo6j0zAezu0G/HAMYgMkhPu4fLK",
# "$2a$12$M8mKpW9a2vZ7PYhq/8eJVOUtKxpo6j0zAezu0G/HAMYgMkhPu4fLK"),
("test", # \/
"$2a$04$yjDgE74RJkeqC0/1NheSScrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIS",
"$2a$04$yjDgE74RJkeqC0/1NheSSOrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIS"),
# bad checksum padding
("test", # \/
"$2a$04$yjDgE74RJkeqC0/1NheSSOrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIV",
"$2a$04$yjDgE74RJkeqC0/1NheSSOrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIS"),
]
def test_90_bcrypt_padding(self):
"test passlib correctly handles bcrypt padding bits"
self.require_TEST_MODE("full")
#
# prevents reccurrence of issue 25 (https://code.google.com/p/passlib/issues/detail?id=25)
# were some unused bits were incorrectly set in bcrypt salt strings.
# (fixed since 1.5.3)
#
bcrypt = self.handler
corr_desc = ".*incorrectly set padding bits"
#
# test encrypt() / genconfig() don't generate invalid salts anymore
#
def check_padding(hash):
assert hash.startswith("$2a$") and len(hash) >= 28
self.assertTrue(hash[28] in '.Oeu',
"unused bits incorrectly set in hash: %r" % (hash,))
for i in irange(6):
check_padding(bcrypt.genconfig())
for i in irange(3):
check_padding(bcrypt.encrypt("bob", rounds=bcrypt.min_rounds))
#
# test genconfig() corrects invalid salts & issues warning.
#
with self.assertWarningList(["salt too large", corr_desc]):
hash = bcrypt.genconfig(salt="."*21 + "A.", rounds=5, relaxed=True)
self.assertEqual(hash, "$2a$05$" + "." * 22)
#
# make sure genhash() corrects input
#
samples = self.known_incorrect_padding
for pwd, bad, good in samples:
with self.assertWarningList([corr_desc]):
self.assertEqual(bcrypt.genhash(pwd, bad), good)
with self.assertWarningList([]):
self.assertEqual(bcrypt.genhash(pwd, good), good)
#
# and that verify() works good & bad
#
with self.assertWarningList([corr_desc]):
self.assertTrue(bcrypt.verify(pwd, bad))
with self.assertWarningList([]):
self.assertTrue(bcrypt.verify(pwd, good))
#
# test normhash cleans things up correctly
#
for pwd, bad, good in samples:
with self.assertWarningList([corr_desc]):
self.assertEqual(bcrypt.normhash(bad), good)
with self.assertWarningList([]):
self.assertEqual(bcrypt.normhash(good), good)
self.assertEqual(bcrypt.normhash("$md5$abc"), "$md5$abc")
hash.bcrypt._no_backends_msg() # call this for coverage purposes
# create test cases for specific backends
bcrypt_bcrypt_test, bcrypt_pybcrypt_test, bcrypt_bcryptor_test, bcrypt_os_crypt_test, bcrypt_builtin_test = \
_bcrypt_test.create_backend_cases(["bcrypt", "pybcrypt", "bcryptor", "os_crypt", "builtin"])
#=============================================================================
# bcrypt
#=============================================================================
class _bcrypt_sha256_test(HandlerCase):
"base for BCrypt-SHA256 test cases"
handler = hash.bcrypt_sha256
reduce_default_rounds = True
forbidden_characters = None
fuzz_salts_need_bcrypt_repair = True
fallback_os_crypt_handler = hash.bcrypt
known_correct_hashes = [
#
# custom test vectors
#
# empty
("",
'$bcrypt-sha256$2a,5$E/e/2AOhqM5W/KJTFQzLce$F6dYSxOdAEoJZO2eoHUZWZljW/e0TXO'),
# ascii
("password",
'$bcrypt-sha256$2a,5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu'),
# unicode / utf8
(UPASS_TABLE,
'$bcrypt-sha256$2a,5$.US1fQ4TQS.ZTz/uJ5Kyn.$QNdPDOTKKT5/sovNz1iWg26quOU4Pje'),
(UPASS_TABLE.encode("utf-8"),
'$bcrypt-sha256$2a,5$.US1fQ4TQS.ZTz/uJ5Kyn.$QNdPDOTKKT5/sovNz1iWg26quOU4Pje'),
# test >72 chars is hashed correctly -- under bcrypt these hash the same.
# NOTE: test_60_secret_size() handles this already, this is just for overkill :)
(repeat_string("abc123",72),
'$bcrypt-sha256$2a,5$X1g1nh3g0v4h6970O68cxe$r/hyEtqJ0teqPEmfTLoZ83ciAI1Q74.'),
(repeat_string("abc123",72)+"qwr",
'$bcrypt-sha256$2a,5$X1g1nh3g0v4h6970O68cxe$021KLEif6epjot5yoxk0m8I0929ohEa'),
(repeat_string("abc123",72)+"xyz",
'$bcrypt-sha256$2a,5$X1g1nh3g0v4h6970O68cxe$7.1kgpHduMGEjvM3fX6e/QCvfn6OKja'),
]
known_correct_configs =[
('$bcrypt-sha256$2a,5$5Hg1DKFqPE8C2aflZ5vVoe',
"password", '$bcrypt-sha256$2a,5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu'),
]
known_malformed_hashes = [
# bad char in otherwise correct hash
# \/
'$bcrypt-sha256$2a,5$5Hg1DKF!PE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
# unrecognized bcrypt variant
'$bcrypt-sha256$2c,5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
# unsupported bcrypt variant
'$bcrypt-sha256$2x,5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
# rounds zero-padded
'$bcrypt-sha256$2a,05$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
# config string w/ $ added
'$bcrypt-sha256$2a,5$5Hg1DKFqPE8C2aflZ5vVoe$',
]
#===================================================================
# override some methods -- cloned from bcrypt
#===================================================================
def setUp(self):
# ensure builtin is enabled for duration of test.
if TEST_MODE("full") and self.backend == "builtin":
key = "PASSLIB_BUILTIN_BCRYPT"
orig = os.environ.get(key)
if orig:
self.addCleanup(os.environ.__setitem__, key, orig)
else:
self.addCleanup(os.environ.__delitem__, key)
os.environ[key] = "enabled"
super(_bcrypt_sha256_test, self).setUp()
def populate_settings(self, kwds):
# builtin is still just way too slow.
if self.backend == "builtin":
kwds.setdefault("rounds", 4)
super(_bcrypt_sha256_test, self).populate_settings(kwds)
#===================================================================
# override ident tests for now
#===================================================================
def test_30_HasManyIdents(self):
raise self.skipTest("multiple idents not supported")
def test_30_HasOneIdent(self):
# forbidding ident keyword, we only support "2a" for now
handler = self.handler
handler(use_defaults=True)
self.assertRaises(ValueError, handler, ident="$2y$", use_defaults=True)
#===================================================================
# fuzz testing -- cloned from bcrypt
#===================================================================
def fuzz_setting_rounds(self):
# decrease default rounds for fuzz testing to speed up volume.
return randintgauss(5, 8, 6, 1)
# create test cases for specific backends
bcrypt_sha256_bcrypt_test, bcrypt_sha256_pybcrypt_test, bcrypt_sha256_bcryptor_test, bcrypt_sha256_os_crypt_test, bcrypt_sha256_builtin_test = \
_bcrypt_sha256_test.create_backend_cases(["bcrypt", "pybcrypt", "bcryptor", "os_crypt", "builtin"])
#=============================================================================
# eof
#=============================================================================

View File

@ -0,0 +1,366 @@
"""passlib.tests.test_handlers_django - tests for passlib hash algorithms"""
#=============================================================================
# imports
#=============================================================================
from __future__ import with_statement
# core
import hashlib
import logging; log = logging.getLogger(__name__)
import os
import warnings
# site
# pkg
from passlib import hash
from passlib.utils import repeat_string
from passlib.utils.compat import irange, PY3, u, get_method_function
from passlib.tests.utils import TestCase, HandlerCase, skipUnless, \
TEST_MODE, b, catch_warnings, UserHandlerMixin, randintgauss, EncodingHandlerMixin
from passlib.tests.test_handlers import UPASS_WAV, UPASS_USD, UPASS_TABLE
# module
#=============================================================================
# django
#=============================================================================
# standard string django uses
UPASS_LETMEIN = u('l\xe8tmein')
def vstr(version):
return ".".join(str(e) for e in version)
class _DjangoHelper(object):
# NOTE: not testing against Django < 1.0 since it doesn't support
# most of these hash formats.
# flag that hash wasn't added until specified version
min_django_version = ()
def fuzz_verifier_django(self):
from passlib.tests.test_ext_django import DJANGO_VERSION
# check_password() not added until 1.0
min_django_version = max(self.min_django_version, (1,0))
if DJANGO_VERSION < min_django_version:
return None
from django.contrib.auth.models import check_password
def verify_django(secret, hash):
"django/check_password"
if (1,4) <= DJANGO_VERSION < (1,6) and not secret:
return "skip"
if self.handler.name == "django_bcrypt" and hash.startswith("bcrypt$$2y$"):
hash = hash.replace("$$2y$", "$$2a$")
if DJANGO_VERSION >= (1,5) and self.django_has_encoding_glitch and isinstance(secret, bytes):
# e.g. unsalted_md5 on 1.5 and higher try to combine
# salt + password before encoding to bytes, leading to ascii error.
# this works around that issue.
secret = secret.decode("utf-8")
return check_password(secret, hash)
return verify_django
def test_90_django_reference(self):
"run known correct hashes through Django's check_password()"
from passlib.tests.test_ext_django import DJANGO_VERSION
# check_password() not added until 1.0
min_django_version = max(self.min_django_version, (1,0))
if DJANGO_VERSION < min_django_version:
raise self.skipTest("Django >= %s not installed" % vstr(min_django_version))
from django.contrib.auth.models import check_password
assert self.known_correct_hashes
for secret, hash in self.iter_known_hashes():
if (1,4) <= DJANGO_VERSION < (1,6) and not secret:
# django 1.4-1.5 rejects empty passwords
self.assertFalse(check_password(secret, hash),
"empty string should not have verified")
continue
self.assertTrue(check_password(secret, hash),
"secret=%r hash=%r failed to verify" %
(secret, hash))
self.assertFalse(check_password('x' + secret, hash),
"mangled secret=%r hash=%r incorrect verified" %
(secret, hash))
django_has_encoding_glitch = False
def test_91_django_generation(self):
"test against output of Django's make_password()"
from passlib.tests.test_ext_django import DJANGO_VERSION
# make_password() not added until 1.4
min_django_version = max(self.min_django_version, (1,4))
if DJANGO_VERSION < min_django_version:
raise self.skipTest("Django >= %s not installed" % vstr(min_django_version))
from passlib.utils import tick
from django.contrib.auth.hashers import make_password
name = self.handler.django_name # set for all the django_* handlers
end = tick() + self.max_fuzz_time/2
while tick() < end:
secret, other = self.get_fuzz_password_pair()
if not secret: # django 1.4 rejects empty passwords.
continue
if DJANGO_VERSION >= (1,5) and self.django_has_encoding_glitch and isinstance(secret, bytes):
# e.g. unsalted_md5 on 1.5 and higher try to combine
# salt + password before encoding to bytes, leading to ascii error.
# this works around that issue.
secret = secret.decode("utf-8")
hash = make_password(secret, hasher=name)
self.assertTrue(self.do_identify(hash))
self.assertTrue(self.do_verify(secret, hash))
self.assertFalse(self.do_verify(other, hash))
class django_disabled_test(HandlerCase):
"test django_disabled"
handler = hash.django_disabled
is_disabled_handler = True
known_correct_hashes = [
# *everything* should hash to "!", and nothing should verify
("password", "!"),
("", "!"),
(UPASS_TABLE, "!"),
]
known_alternate_hashes = [
# django 1.6 appends random alpnum string
("!9wa845vn7098ythaehasldkfj", "password", "!"),
]
class django_des_crypt_test(HandlerCase, _DjangoHelper):
"test django_des_crypt"
handler = hash.django_des_crypt
secret_size = 8
known_correct_hashes = [
# ensures only first two digits of salt count.
("password", 'crypt$c2$c2M87q...WWcU'),
("password", 'crypt$c2e86$c2M87q...WWcU'),
("passwordignoreme", 'crypt$c2.AZ$c2M87q...WWcU'),
# ensures utf-8 used for unicode
(UPASS_USD, 'crypt$c2e86$c2hN1Bxd6ZiWs'),
(UPASS_TABLE, 'crypt$0.aQs$0.wB.TT0Czvlo'),
(u("hell\u00D6"), "crypt$sa$saykDgk3BPZ9E"),
# prevent regression of issue 22
("foo", 'crypt$MNVY.9ajgdvDQ$MNVY.9ajgdvDQ'),
]
known_alternate_hashes = [
# ensure django 1.4 empty salt field is accepted;
# but that salt field is re-filled (for django 1.0 compatibility)
('crypt$$c2M87q...WWcU', "password", 'crypt$c2$c2M87q...WWcU'),
]
known_unidentified_hashes = [
'sha1$aa$bb',
]
known_malformed_hashes = [
# checksum too short
'crypt$c2$c2M87q',
# salt must be >2
'crypt$f$c2M87q...WWcU',
# make sure first 2 chars of salt & chk field agree.
'crypt$ffe86$c2M87q...WWcU',
]
class django_salted_md5_test(HandlerCase, _DjangoHelper):
"test django_salted_md5"
handler = hash.django_salted_md5
django_has_encoding_glitch = True
known_correct_hashes = [
# test extra large salt
("password", 'md5$123abcdef$c8272612932975ee80e8a35995708e80'),
# test django 1.4 alphanumeric salt
("test", 'md5$3OpqnFAHW5CT$54b29300675271049a1ebae07b395e20'),
# ensures utf-8 used for unicode
(UPASS_USD, 'md5$c2e86$92105508419a81a6babfaecf876a2fa0'),
(UPASS_TABLE, 'md5$d9eb8$01495b32852bffb27cf5d4394fe7a54c'),
]
known_unidentified_hashes = [
'sha1$aa$bb',
]
known_malformed_hashes = [
# checksum too short
'md5$aa$bb',
]
def fuzz_setting_salt_size(self):
# workaround for django14 regression --
# 1.4 won't accept hashes with empty salt strings, unlike 1.3 and earlier.
# looks to be fixed in a future release -- https://code.djangoproject.com/ticket/18144
# for now, we avoid salt_size==0 under 1.4
handler = self.handler
from passlib.tests.test_ext_django import has_django14
default = handler.default_salt_size
assert handler.min_salt_size == 0
lower = 1 if has_django14 else 0
upper = handler.max_salt_size or default*4
return randintgauss(lower, upper, default, default*.5)
class django_salted_sha1_test(HandlerCase, _DjangoHelper):
"test django_salted_sha1"
handler = hash.django_salted_sha1
django_has_encoding_glitch = True
known_correct_hashes = [
# test extra large salt
("password",'sha1$123abcdef$e4a1877b0e35c47329e7ed7e58014276168a37ba'),
# test django 1.4 alphanumeric salt
("test", 'sha1$bcwHF9Hy8lxS$6b4cfa0651b43161c6f1471ce9523acf1f751ba3'),
# ensures utf-8 used for unicode
(UPASS_USD, 'sha1$c2e86$0f75c5d7fbd100d587c127ef0b693cde611b4ada'),
(UPASS_TABLE, 'sha1$6d853$ef13a4d8fb57aed0cb573fe9c82e28dc7fd372d4'),
# generic password
("MyPassword", 'sha1$54123$893cf12e134c3c215f3a76bd50d13f92404a54d3'),
]
known_unidentified_hashes = [
'md5$aa$bb',
]
known_malformed_hashes = [
# checksum too short
'sha1$c2e86$0f75',
]
fuzz_setting_salt_size = get_method_function(django_salted_md5_test.fuzz_setting_salt_size)
class django_pbkdf2_sha256_test(HandlerCase, _DjangoHelper):
"test django_pbkdf2_sha256"
handler = hash.django_pbkdf2_sha256
min_django_version = (1,4)
known_correct_hashes = [
#
# custom - generated via django 1.4 hasher
#
('not a password',
'pbkdf2_sha256$10000$kjVJaVz6qsnJ$5yPHw3rwJGECpUf70daLGhOrQ5+AMxIJdz1c3bqK1Rs='),
(UPASS_TABLE,
'pbkdf2_sha256$10000$bEwAfNrH1TlQ$OgYUblFNUX1B8GfMqaCYUK/iHyO0pa7STTDdaEJBuY0='),
]
class django_pbkdf2_sha1_test(HandlerCase, _DjangoHelper):
"test django_pbkdf2_sha1"
handler = hash.django_pbkdf2_sha1
min_django_version = (1,4)
known_correct_hashes = [
#
# custom - generated via django 1.4 hashers
#
('not a password',
'pbkdf2_sha1$10000$wz5B6WkasRoF$atJmJ1o+XfJxKq1+Nu1f1i57Z5I='),
(UPASS_TABLE,
'pbkdf2_sha1$10000$KZKWwvqb8BfL$rw5pWsxJEU4JrZAQhHTCO+u0f5Y='),
]
class django_bcrypt_test(HandlerCase, _DjangoHelper):
"test django_bcrypt"
handler = hash.django_bcrypt
secret_size = 72
min_django_version = (1,4)
fuzz_salts_need_bcrypt_repair = True
known_correct_hashes = [
#
# just copied and adapted a few test vectors from bcrypt (above),
# since django_bcrypt is just a wrapper for the real bcrypt class.
#
('', 'bcrypt$$2a$06$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s.'),
('abcdefghijklmnopqrstuvwxyz',
'bcrypt$$2a$10$fVH8e28OQRj9tqiDXs1e1uxpsjN0c7II7YPKXua2NAKYvM6iQk7dq'),
(UPASS_TABLE,
'bcrypt$$2a$05$Z17AXnnlpzddNUvnC6cZNOSwMA/8oNiKnHTHTwLlBijfucQQlHjaG'),
]
# NOTE: the following have been cloned from _bcrypt_test()
def populate_settings(self, kwds):
# speed up test w/ lower rounds
kwds.setdefault("rounds", 4)
super(django_bcrypt_test, self).populate_settings(kwds)
def fuzz_setting_rounds(self):
# decrease default rounds for fuzz testing to speed up volume.
return randintgauss(5, 8, 6, 1)
def fuzz_setting_ident(self):
# omit multi-ident tests, only $2a$ counts for this class
return None
django_bcrypt_test = skipUnless(hash.bcrypt.has_backend(),
"no bcrypt backends available")(django_bcrypt_test)
class django_bcrypt_sha256_test(HandlerCase, _DjangoHelper):
"test django_bcrypt_sha256"
handler = hash.django_bcrypt_sha256
min_django_version = (1,6)
forbidden_characters = None
fuzz_salts_need_bcrypt_repair = True
known_correct_hashes = [
#
# custom - generated via django 1.6 hasher
#
('',
'bcrypt_sha256$$2a$06$/3OeRpbOf8/l6nPPRdZPp.nRiyYqPobEZGdNRBWihQhiFDh1ws1tu'),
(UPASS_LETMEIN,
'bcrypt_sha256$$2a$08$NDjSAIcas.EcoxCRiArvT.MkNiPYVhrsrnJsRkLueZOoV1bsQqlmC'),
(UPASS_TABLE,
'bcrypt_sha256$$2a$06$kCXUnRFQptGg491siDKNTu8RxjBGSjALHRuvhPYNFsa4Ea5d9M48u'),
# test >72 chars is hashed correctly -- under bcrypt these hash the same.
(repeat_string("abc123",72),
'bcrypt_sha256$$2a$06$Tg/oYyZTyAf.Nb3qSgN61OySmyXA8FoY4PjGizjE1QSDfuL5MXNni'),
(repeat_string("abc123",72)+"qwr",
'bcrypt_sha256$$2a$06$Tg/oYyZTyAf.Nb3qSgN61Ocy0BEz1RK6xslSNi8PlaLX2pe7x/KQG'),
(repeat_string("abc123",72)+"xyz",
'bcrypt_sha256$$2a$06$Tg/oYyZTyAf.Nb3qSgN61OvY2zoRVUa2Pugv2ExVOUT2YmhvxUFUa'),
]
known_malformed_hashers = [
# data in django salt field
'bcrypt_sha256$xyz$2a$06$/3OeRpbOf8/l6nPPRdZPp.nRiyYqPobEZGdNRBWihQhiFDh1ws1tu',
]
def test_30_HasManyIdents(self):
raise self.skipTest("multiple idents not supported")
def test_30_HasOneIdent(self):
# forbidding ident keyword, django doesn't support configuring this
handler = self.handler
handler(use_defaults=True)
self.assertRaises(TypeError, handler, ident="$2a$", use_defaults=True)
# NOTE: the following have been cloned from _bcrypt_test()
def populate_settings(self, kwds):
# speed up test w/ lower rounds
kwds.setdefault("rounds", 4)
super(django_bcrypt_sha256_test, self).populate_settings(kwds)
def fuzz_setting_rounds(self):
# decrease default rounds for fuzz testing to speed up volume.
return randintgauss(5, 8, 6, 1)
def fuzz_setting_ident(self):
# omit multi-ident tests, only $2a$ counts for this class
return None
django_bcrypt_sha256_test = skipUnless(hash.bcrypt.has_backend(),
"no bcrypt backends available")(django_bcrypt_sha256_test)
#=============================================================================
# eof
#=============================================================================

View File

@ -0,0 +1,98 @@
"""test passlib.hosts"""
#=============================================================================
# imports
#=============================================================================
from __future__ import with_statement
# core
import logging; log = logging.getLogger(__name__)
import warnings
# site
# pkg
from passlib import hosts, hash as hashmod
from passlib.utils import unix_crypt_schemes
from passlib.tests.utils import TestCase
# module
#=============================================================================
# test predefined app contexts
#=============================================================================
class HostsTest(TestCase):
"perform general tests to make sure contexts work"
# NOTE: these tests are not really comprehensive,
# since they would do little but duplicate
# the presets in apps.py
#
# they mainly try to ensure no typos
# or dynamic behavior foul-ups.
def check_unix_disabled(self, ctx):
for hash in [
"",
"!",
"*",
"!$1$TXl/FX/U$BZge.lr.ux6ekjEjxmzwz0",
]:
self.assertEqual(ctx.identify(hash), 'unix_disabled')
self.assertFalse(ctx.verify('test', hash))
def test_linux_context(self):
ctx = hosts.linux_context
for hash in [
('$6$rounds=41128$VoQLvDjkaZ6L6BIE$4pt.1Ll1XdDYduEwEYPCMOBiR6W6'
'znsyUEoNlcVXpv2gKKIbQolgmTGe6uEEVJ7azUxuc8Tf7zV9SD2z7Ij751'),
('$5$rounds=31817$iZGmlyBQ99JSB5n6$p4E.pdPBWx19OajgjLRiOW0itGny'
'xDGgMlDcOsfaI17'),
'$1$TXl/FX/U$BZge.lr.ux6ekjEjxmzwz0',
'kAJJz.Rwp0A/I',
]:
self.assertTrue(ctx.verify("test", hash))
self.check_unix_disabled(ctx)
def test_bsd_contexts(self):
for ctx in [
hosts.freebsd_context,
hosts.openbsd_context,
hosts.netbsd_context,
]:
for hash in [
'$1$TXl/FX/U$BZge.lr.ux6ekjEjxmzwz0',
'kAJJz.Rwp0A/I',
]:
self.assertTrue(ctx.verify("test", hash))
h1 = "$2a$04$yjDgE74RJkeqC0/1NheSSOrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIS"
if hashmod.bcrypt.has_backend():
self.assertTrue(ctx.verify("test", h1))
else:
self.assertEqual(ctx.identify(h1), "bcrypt")
self.check_unix_disabled(ctx)
def test_host_context(self):
ctx = getattr(hosts, "host_context", None)
if not ctx:
return self.skipTest("host_context not available on this platform")
# validate schemes is non-empty,
# and contains unix_disabled + at least one real scheme
schemes = list(ctx.schemes())
self.assertTrue(schemes, "appears to be unix system, but no known schemes supported by crypt")
self.assertTrue('unix_disabled' in schemes)
schemes.remove("unix_disabled")
self.assertTrue(schemes, "should have schemes beside fallback scheme")
self.assertTrue(set(unix_crypt_schemes).issuperset(schemes))
# check for hash support
self.check_unix_disabled(ctx)
for scheme, hash in [
("sha512_crypt", ('$6$rounds=41128$VoQLvDjkaZ6L6BIE$4pt.1Ll1XdDYduEwEYPCMOBiR6W6'
'znsyUEoNlcVXpv2gKKIbQolgmTGe6uEEVJ7azUxuc8Tf7zV9SD2z7Ij751')),
("sha256_crypt", ('$5$rounds=31817$iZGmlyBQ99JSB5n6$p4E.pdPBWx19OajgjLRiOW0itGny'
'xDGgMlDcOsfaI17')),
("md5_crypt", '$1$TXl/FX/U$BZge.lr.ux6ekjEjxmzwz0'),
("des_crypt", 'kAJJz.Rwp0A/I'),
]:
if scheme in schemes:
self.assertTrue(ctx.verify("test", hash))
#=============================================================================
# eof
#=============================================================================

View File

@ -0,0 +1,214 @@
"""tests for passlib.pwhash -- (c) Assurance Technologies 2003-2009"""
#=============================================================================
# imports
#=============================================================================
from __future__ import with_statement
# core
import hashlib
from logging import getLogger
import os
import time
import warnings
import sys
# site
# pkg
from passlib import hash, registry
from passlib.registry import register_crypt_handler, register_crypt_handler_path, \
get_crypt_handler, list_crypt_handlers, _unload_handler_name as unload_handler_name
import passlib.utils.handlers as uh
from passlib.tests.utils import TestCase, catch_warnings
# module
log = getLogger(__name__)
#=============================================================================
# dummy handlers
#
# NOTE: these are defined outside of test case
# since they're used by test_register_crypt_handler_path(),
# which needs them to be available as module globals.
#=============================================================================
class dummy_0(uh.StaticHandler):
name = "dummy_0"
class alt_dummy_0(uh.StaticHandler):
name = "dummy_0"
dummy_x = 1
#=============================================================================
# test registry
#=============================================================================
class RegistryTest(TestCase):
descriptionPrefix = "passlib registry"
def tearDown(self):
for name in ("dummy_0", "dummy_1", "dummy_x", "dummy_bad"):
unload_handler_name(name)
def test_hash_proxy(self):
"test passlib.hash proxy object"
# check dir works
dir(hash)
# check repr works
repr(hash)
# check non-existent attrs raise error
self.assertRaises(AttributeError, getattr, hash, 'fooey')
# GAE tries to set __loader__,
# make sure that doesn't call register_crypt_handler.
old = getattr(hash, "__loader__", None)
test = object()
hash.__loader__ = test
self.assertIs(hash.__loader__, test)
if old is None:
del hash.__loader__
self.assertFalse(hasattr(hash, "__loader__"))
else:
hash.__loader__ = old
self.assertIs(hash.__loader__, old)
# check storing attr calls register_crypt_handler
class dummy_1(uh.StaticHandler):
name = "dummy_1"
hash.dummy_1 = dummy_1
self.assertIs(get_crypt_handler("dummy_1"), dummy_1)
# check storing under wrong name results in error
self.assertRaises(ValueError, setattr, hash, "dummy_1x", dummy_1)
def test_register_crypt_handler_path(self):
"test register_crypt_handler_path()"
# NOTE: this messes w/ internals of registry, shouldn't be used publically.
paths = registry._locations
# check namespace is clear
self.assertTrue('dummy_0' not in paths)
self.assertFalse(hasattr(hash, 'dummy_0'))
# check invalid names are rejected
self.assertRaises(ValueError, register_crypt_handler_path,
"dummy_0", ".test_registry")
self.assertRaises(ValueError, register_crypt_handler_path,
"dummy_0", __name__ + ":dummy_0:xxx")
self.assertRaises(ValueError, register_crypt_handler_path,
"dummy_0", __name__ + ":dummy_0.xxx")
# try lazy load
register_crypt_handler_path('dummy_0', __name__)
self.assertTrue('dummy_0' in list_crypt_handlers())
self.assertTrue('dummy_0' not in list_crypt_handlers(loaded_only=True))
self.assertIs(hash.dummy_0, dummy_0)
self.assertTrue('dummy_0' in list_crypt_handlers(loaded_only=True))
unload_handler_name('dummy_0')
# try lazy load w/ alt
register_crypt_handler_path('dummy_0', __name__ + ':alt_dummy_0')
self.assertIs(hash.dummy_0, alt_dummy_0)
unload_handler_name('dummy_0')
# check lazy load w/ wrong type fails
register_crypt_handler_path('dummy_x', __name__)
self.assertRaises(TypeError, get_crypt_handler, 'dummy_x')
# check lazy load w/ wrong name fails
register_crypt_handler_path('alt_dummy_0', __name__)
self.assertRaises(ValueError, get_crypt_handler, "alt_dummy_0")
# TODO: check lazy load which calls register_crypt_handler (warning should be issued)
sys.modules.pop("passlib.tests._test_bad_register", None)
register_crypt_handler_path("dummy_bad", "passlib.tests._test_bad_register")
with catch_warnings():
warnings.filterwarnings("ignore", "xxxxxxxxxx", DeprecationWarning)
h = get_crypt_handler("dummy_bad")
from passlib.tests import _test_bad_register as tbr
self.assertIs(h, tbr.alt_dummy_bad)
def test_register_crypt_handler(self):
"test register_crypt_handler()"
self.assertRaises(TypeError, register_crypt_handler, {})
self.assertRaises(ValueError, register_crypt_handler, type('x', (uh.StaticHandler,), dict(name=None)))
self.assertRaises(ValueError, register_crypt_handler, type('x', (uh.StaticHandler,), dict(name="AB_CD")))
self.assertRaises(ValueError, register_crypt_handler, type('x', (uh.StaticHandler,), dict(name="ab-cd")))
self.assertRaises(ValueError, register_crypt_handler, type('x', (uh.StaticHandler,), dict(name="ab__cd")))
self.assertRaises(ValueError, register_crypt_handler, type('x', (uh.StaticHandler,), dict(name="default")))
class dummy_1(uh.StaticHandler):
name = "dummy_1"
class dummy_1b(uh.StaticHandler):
name = "dummy_1"
self.assertTrue('dummy_1' not in list_crypt_handlers())
register_crypt_handler(dummy_1)
register_crypt_handler(dummy_1)
self.assertIs(get_crypt_handler("dummy_1"), dummy_1)
self.assertRaises(KeyError, register_crypt_handler, dummy_1b)
self.assertIs(get_crypt_handler("dummy_1"), dummy_1)
register_crypt_handler(dummy_1b, force=True)
self.assertIs(get_crypt_handler("dummy_1"), dummy_1b)
self.assertTrue('dummy_1' in list_crypt_handlers())
def test_get_crypt_handler(self):
"test get_crypt_handler()"
class dummy_1(uh.StaticHandler):
name = "dummy_1"
# without available handler
self.assertRaises(KeyError, get_crypt_handler, "dummy_1")
self.assertIs(get_crypt_handler("dummy_1", None), None)
# already loaded handler
register_crypt_handler(dummy_1)
self.assertIs(get_crypt_handler("dummy_1"), dummy_1)
with catch_warnings():
warnings.filterwarnings("ignore", "handler names should be lower-case, and use underscores instead of hyphens:.*", UserWarning)
# already loaded handler, using incorrect name
self.assertIs(get_crypt_handler("DUMMY-1"), dummy_1)
# lazy load of unloaded handler, using incorrect name
register_crypt_handler_path('dummy_0', __name__)
self.assertIs(get_crypt_handler("DUMMY-0"), dummy_0)
# check system & private names aren't returned
import passlib.hash # ensure module imported, so py3.3 sets __package__
passlib.hash.__dict__["_fake"] = "dummy" # so behavior seen under py2x also
for name in ["_fake", "__package__"]:
self.assertRaises(KeyError, get_crypt_handler, name)
self.assertIs(get_crypt_handler(name, None), None)
def test_list_crypt_handlers(self):
"test list_crypt_handlers()"
from passlib.registry import list_crypt_handlers
# check system & private names aren't returned
import passlib.hash # ensure module imported, so py3.3 sets __package__
passlib.hash.__dict__["_fake"] = "dummy" # so behavior seen under py2x also
for name in list_crypt_handlers():
self.assertFalse(name.startswith("_"), "%r: " % name)
def test_handlers(self):
"verify we have tests for all handlers"
from passlib.registry import list_crypt_handlers
from passlib.tests.test_handlers import get_handler_case
for name in list_crypt_handlers():
if name.startswith("ldap_") and name[5:] in list_crypt_handlers():
continue
if name in ["roundup_plaintext"]:
continue
self.assertTrue(get_handler_case(name))
#=============================================================================
# eof
#=============================================================================

917
passlib/tests/test_utils.py Normal file
View File

@ -0,0 +1,917 @@
"""tests for passlib.util"""
#=============================================================================
# imports
#=============================================================================
from __future__ import with_statement
# core
from binascii import hexlify, unhexlify
import sys
import random
import warnings
# site
# pkg
# module
from passlib.utils.compat import b, bytes, bascii_to_str, irange, PY2, PY3, u, \
unicode, join_bytes, SUPPORTS_DIR_METHOD
from passlib.tests.utils import TestCase, catch_warnings
def hb(source):
return unhexlify(b(source))
#=============================================================================
# byte funcs
#=============================================================================
class MiscTest(TestCase):
"tests various parts of utils module"
# NOTE: could test xor_bytes(), but it's exercised well enough by pbkdf2 test
def test_compat(self):
"test compat's lazymodule"
from passlib.utils import compat
# "<module 'passlib.utils.compat' from 'passlib/utils/compat.pyc'>"
self.assertRegex(repr(compat),
r"^<module 'passlib.utils.compat' from '.*?'>$")
# test synthentic dir()
dir(compat)
if SUPPORTS_DIR_METHOD:
self.assertTrue('UnicodeIO' in dir(compat))
self.assertTrue('irange' in dir(compat))
def test_classproperty(self):
from passlib.utils import classproperty
class test(object):
xvar = 1
@classproperty
def xprop(cls):
return cls.xvar
self.assertEqual(test.xprop, 1)
prop = test.__dict__['xprop']
self.assertIs(prop.im_func, prop.__func__)
def test_deprecated_function(self):
from passlib.utils import deprecated_function
# NOTE: not comprehensive, just tests the basic behavior
@deprecated_function(deprecated="1.6", removed="1.8")
def test_func(*args):
"test docstring"
return args
self.assertTrue(".. deprecated::" in test_func.__doc__)
with self.assertWarningList(dict(category=DeprecationWarning,
message="the function passlib.tests.test_utils.test_func() "
"is deprecated as of Passlib 1.6, and will be "
"removed in Passlib 1.8."
)):
self.assertEqual(test_func(1,2), (1,2))
def test_memoized_property(self):
from passlib.utils import memoized_property
class dummy(object):
counter = 0
@memoized_property
def value(self):
value = self.counter
self.counter = value+1
return value
d = dummy()
self.assertEqual(d.value, 0)
self.assertEqual(d.value, 0)
self.assertEqual(d.counter, 1)
prop = dummy.value
self.assertIs(prop.im_func, prop.__func__)
def test_getrandbytes(self):
"test getrandbytes()"
from passlib.utils import getrandbytes, rng
def f(*a,**k):
return getrandbytes(rng, *a, **k)
self.assertEqual(len(f(0)), 0)
a = f(10)
b = f(10)
self.assertIsInstance(a, bytes)
self.assertEqual(len(a), 10)
self.assertEqual(len(b), 10)
self.assertNotEqual(a, b)
def test_getrandstr(self):
"test getrandstr()"
from passlib.utils import getrandstr, rng
def f(*a,**k):
return getrandstr(rng, *a, **k)
# count 0
self.assertEqual(f('abc',0), '')
# count <0
self.assertRaises(ValueError, f, 'abc', -1)
# letters 0
self.assertRaises(ValueError, f, '', 0)
# letters 1
self.assertEqual(f('a',5), 'aaaaa')
# letters
x = f(u('abc'), 16)
y = f(u('abc'), 16)
self.assertIsInstance(x, unicode)
self.assertNotEqual(x,y)
self.assertEqual(sorted(set(x)), [u('a'),u('b'),u('c')])
# bytes
x = f(b('abc'), 16)
y = f(b('abc'), 16)
self.assertIsInstance(x, bytes)
self.assertNotEqual(x,y)
# NOTE: decoding this due to py3 bytes
self.assertEqual(sorted(set(x.decode("ascii"))), [u('a'),u('b'),u('c')])
# generate_password
from passlib.utils import generate_password
self.assertEqual(len(generate_password(15)), 15)
def test_is_crypt_context(self):
"test is_crypt_context()"
from passlib.utils import is_crypt_context
from passlib.context import CryptContext
cc = CryptContext(["des_crypt"])
self.assertTrue(is_crypt_context(cc))
self.assertFalse(not is_crypt_context(cc))
def test_genseed(self):
"test genseed()"
import random
from passlib.utils import genseed
rng = random.Random(genseed())
a = rng.randint(0, 100000)
rng = random.Random(genseed())
b = rng.randint(0, 100000)
self.assertNotEqual(a,b)
rng.seed(genseed(rng))
def test_crypt(self):
"test crypt.crypt() wrappers"
from passlib.utils import has_crypt, safe_crypt, test_crypt
# test everything is disabled
if not has_crypt:
self.assertEqual(safe_crypt("test", "aa"), None)
self.assertFalse(test_crypt("test", "aaqPiZY5xR5l."))
raise self.skipTest("crypt.crypt() not available")
# XXX: this assumes *every* crypt() implementation supports des_crypt.
# if this fails for some platform, this test will need modifying.
# test return type
self.assertIsInstance(safe_crypt(u("test"), u("aa")), unicode)
# test ascii password
h1 = u('aaqPiZY5xR5l.')
self.assertEqual(safe_crypt(u('test'), u('aa')), h1)
self.assertEqual(safe_crypt(b('test'), b('aa')), h1)
# test utf-8 / unicode password
h2 = u('aahWwbrUsKZk.')
self.assertEqual(safe_crypt(u('test\u1234'), 'aa'), h2)
self.assertEqual(safe_crypt(b('test\xe1\x88\xb4'), 'aa'), h2)
# test latin-1 password
hash = safe_crypt(b('test\xff'), 'aa')
if PY3: # py3 supports utf-8 bytes only.
self.assertEqual(hash, None)
else: # but py2 is fine.
self.assertEqual(hash, u('aaOx.5nbTU/.M'))
# test rejects null chars in password
self.assertRaises(ValueError, safe_crypt, '\x00', 'aa')
# check test_crypt()
h1x = h1[:-1] + 'x'
self.assertTrue(test_crypt("test", h1))
self.assertFalse(test_crypt("test", h1x))
# check crypt returning variant error indicators
# some platforms return None on errors, others empty string,
# The BSDs in some cases return ":"
import passlib.utils as mod
orig = mod._crypt
try:
fake = None
mod._crypt = lambda secret, hash: fake
for fake in [None, "", ":", ":0", "*0"]:
self.assertEqual(safe_crypt("test", "aa"), None)
self.assertFalse(test_crypt("test", h1))
fake = 'xxx'
self.assertEqual(safe_crypt("test", "aa"), "xxx")
finally:
mod._crypt = orig
def test_consteq(self):
"test consteq()"
# NOTE: this test is kind of over the top, but that's only because
# this is used for the critical task of comparing hashes for equality.
from passlib.utils import consteq
# ensure error raises for wrong types
self.assertRaises(TypeError, consteq, u(''), b(''))
self.assertRaises(TypeError, consteq, u(''), 1)
self.assertRaises(TypeError, consteq, u(''), None)
self.assertRaises(TypeError, consteq, b(''), u(''))
self.assertRaises(TypeError, consteq, b(''), 1)
self.assertRaises(TypeError, consteq, b(''), None)
self.assertRaises(TypeError, consteq, None, u(''))
self.assertRaises(TypeError, consteq, None, b(''))
self.assertRaises(TypeError, consteq, 1, u(''))
self.assertRaises(TypeError, consteq, 1, b(''))
# check equal inputs compare correctly
for value in [
u("a"),
u("abc"),
u("\xff\xa2\x12\x00")*10,
]:
self.assertTrue(consteq(value, value), "value %r:" % (value,))
value = value.encode("latin-1")
self.assertTrue(consteq(value, value), "value %r:" % (value,))
# check non-equal inputs compare correctly
for l,r in [
# check same-size comparisons with differing contents fail.
(u("a"), u("c")),
(u("abcabc"), u("zbaabc")),
(u("abcabc"), u("abzabc")),
(u("abcabc"), u("abcabz")),
((u("\xff\xa2\x12\x00")*10)[:-1] + u("\x01"),
u("\xff\xa2\x12\x00")*10),
# check different-size comparisons fail.
(u(""), u("a")),
(u("abc"), u("abcdef")),
(u("abc"), u("defabc")),
(u("qwertyuiopasdfghjklzxcvbnm"), u("abc")),
]:
self.assertFalse(consteq(l, r), "values %r %r:" % (l,r))
self.assertFalse(consteq(r, l), "values %r %r:" % (r,l))
l = l.encode("latin-1")
r = r.encode("latin-1")
self.assertFalse(consteq(l, r), "values %r %r:" % (l,r))
self.assertFalse(consteq(r, l), "values %r %r:" % (r,l))
# TODO: add some tests to ensure we take THETA(strlen) time.
# this might be hard to do reproducably.
# NOTE: below code was used to generate stats for analysis
##from math import log as logb
##import timeit
##multipliers = [ 1<<s for s in irange(9)]
##correct = u"abcdefgh"*(1<<4)
##incorrect = u"abcdxfgh"
##print
##first = True
##for run in irange(1):
## times = []
## chars = []
## for m in multipliers:
## supplied = incorrect * m
## def test():
## self.assertFalse(consteq(supplied,correct))
## ##self.assertFalse(supplied == correct)
## times.append(timeit.timeit(test, number=100000))
## chars.append(len(supplied))
## # output for wolfram alpha
## print ", ".join("{%r, %r}" % (c,round(t,4)) for c,t in zip(chars,times))
## def scale(c):
## return logb(c,2)
## print ", ".join("{%r, %r}" % (scale(c),round(t,4)) for c,t in zip(chars,times))
## # output for spreadsheet
## ##if first:
## ## print "na, " + ", ".join(str(c) for c in chars)
## ## first = False
## ##print ", ".join(str(c) for c in [run] + times)
def test_saslprep(self):
"test saslprep() unicode normalizer"
self.require_stringprep()
from passlib.utils import saslprep as sp
# invalid types
self.assertRaises(TypeError, sp, None)
self.assertRaises(TypeError, sp, 1)
self.assertRaises(TypeError, sp, b(''))
# empty strings
self.assertEqual(sp(u('')), u(''))
self.assertEqual(sp(u('\u00AD')), u(''))
# verify B.1 chars are stripped,
self.assertEqual(sp(u("$\u00AD$\u200D$")), u("$$$"))
# verify C.1.2 chars are replaced with space
self.assertEqual(sp(u("$ $\u00A0$\u3000$")), u("$ $ $ $"))
# verify normalization to KC
self.assertEqual(sp(u("a\u0300")), u("\u00E0"))
self.assertEqual(sp(u("\u00E0")), u("\u00E0"))
# verify various forbidden characters
# control chars
self.assertRaises(ValueError, sp, u("\u0000"))
self.assertRaises(ValueError, sp, u("\u007F"))
self.assertRaises(ValueError, sp, u("\u180E"))
self.assertRaises(ValueError, sp, u("\uFFF9"))
# private use
self.assertRaises(ValueError, sp, u("\uE000"))
# non-characters
self.assertRaises(ValueError, sp, u("\uFDD0"))
# surrogates
self.assertRaises(ValueError, sp, u("\uD800"))
# non-plaintext chars
self.assertRaises(ValueError, sp, u("\uFFFD"))
# non-canon
self.assertRaises(ValueError, sp, u("\u2FF0"))
# change display properties
self.assertRaises(ValueError, sp, u("\u200E"))
self.assertRaises(ValueError, sp, u("\u206F"))
# unassigned code points (as of unicode 3.2)
self.assertRaises(ValueError, sp, u("\u0900"))
self.assertRaises(ValueError, sp, u("\uFFF8"))
# tagging characters
self.assertRaises(ValueError, sp, u("\U000e0001"))
# verify bidi behavior
# if starts with R/AL -- must end with R/AL
self.assertRaises(ValueError, sp, u("\u0627\u0031"))
self.assertEqual(sp(u("\u0627")), u("\u0627"))
self.assertEqual(sp(u("\u0627\u0628")), u("\u0627\u0628"))
self.assertEqual(sp(u("\u0627\u0031\u0628")), u("\u0627\u0031\u0628"))
# if starts with R/AL -- cannot contain L
self.assertRaises(ValueError, sp, u("\u0627\u0041\u0628"))
# if doesn't start with R/AL -- can contain R/AL, but L & EN allowed
self.assertRaises(ValueError, sp, u("x\u0627z"))
self.assertEqual(sp(u("x\u0041z")), u("x\u0041z"))
#------------------------------------------------------
# examples pulled from external sources, to be thorough
#------------------------------------------------------
# rfc 4031 section 3 examples
self.assertEqual(sp(u("I\u00ADX")), u("IX")) # strip SHY
self.assertEqual(sp(u("user")), u("user")) # unchanged
self.assertEqual(sp(u("USER")), u("USER")) # case preserved
self.assertEqual(sp(u("\u00AA")), u("a")) # normalize to KC form
self.assertEqual(sp(u("\u2168")), u("IX")) # normalize to KC form
self.assertRaises(ValueError, sp, u("\u0007")) # forbid control chars
self.assertRaises(ValueError, sp, u("\u0627\u0031")) # invalid bidi
# rfc 3454 section 6 examples
# starts with RAL char, must end with RAL char
self.assertRaises(ValueError, sp, u("\u0627\u0031"))
self.assertEqual(sp(u("\u0627\u0031\u0628")), u("\u0627\u0031\u0628"))
def test_splitcomma(self):
from passlib.utils import splitcomma
self.assertEqual(splitcomma(""), [])
self.assertEqual(splitcomma(","), [])
self.assertEqual(splitcomma("a"), ['a'])
self.assertEqual(splitcomma(" a , "), ['a'])
self.assertEqual(splitcomma(" a , b"), ['a', 'b'])
self.assertEqual(splitcomma(" a, b, "), ['a', 'b'])
#=============================================================================
# byte/unicode helpers
#=============================================================================
class CodecTest(TestCase):
"tests bytes/unicode helpers in passlib.utils"
def test_bytes(self):
"test b() helper, bytes and native str type"
if PY3:
import builtins
self.assertIs(bytes, builtins.bytes)
else:
import __builtin__ as builtins
self.assertIs(bytes, builtins.str)
self.assertIsInstance(b(''), bytes)
self.assertIsInstance(b('\x00\xff'), bytes)
if PY3:
self.assertEqual(b('\x00\xff').decode("latin-1"), "\x00\xff")
else:
self.assertEqual(b('\x00\xff'), "\x00\xff")
def test_to_bytes(self):
"test to_bytes()"
from passlib.utils import to_bytes
# check unicode inputs
self.assertEqual(to_bytes(u('abc')), b('abc'))
self.assertEqual(to_bytes(u('\x00\xff')), b('\x00\xc3\xbf'))
# check unicode w/ encodings
self.assertEqual(to_bytes(u('\x00\xff'), 'latin-1'), b('\x00\xff'))
self.assertRaises(ValueError, to_bytes, u('\x00\xff'), 'ascii')
# check bytes inputs
self.assertEqual(to_bytes(b('abc')), b('abc'))
self.assertEqual(to_bytes(b('\x00\xff')), b('\x00\xff'))
self.assertEqual(to_bytes(b('\x00\xc3\xbf')), b('\x00\xc3\xbf'))
# check byte inputs ignores enocding
self.assertEqual(to_bytes(b('\x00\xc3\xbf'), "latin-1"),
b('\x00\xc3\xbf'))
# check bytes transcoding
self.assertEqual(to_bytes(b('\x00\xc3\xbf'), "latin-1", "", "utf-8"),
b('\x00\xff'))
# check other
self.assertRaises(AssertionError, to_bytes, 'abc', None)
self.assertRaises(TypeError, to_bytes, None)
def test_to_unicode(self):
"test to_unicode()"
from passlib.utils import to_unicode
# check unicode inputs
self.assertEqual(to_unicode(u('abc')), u('abc'))
self.assertEqual(to_unicode(u('\x00\xff')), u('\x00\xff'))
# check unicode input ignores encoding
self.assertEqual(to_unicode(u('\x00\xff'), "ascii"), u('\x00\xff'))
# check bytes input
self.assertEqual(to_unicode(b('abc')), u('abc'))
self.assertEqual(to_unicode(b('\x00\xc3\xbf')), u('\x00\xff'))
self.assertEqual(to_unicode(b('\x00\xff'), 'latin-1'),
u('\x00\xff'))
self.assertRaises(ValueError, to_unicode, b('\x00\xff'))
# check other
self.assertRaises(AssertionError, to_unicode, 'abc', None)
self.assertRaises(TypeError, to_unicode, None)
def test_to_native_str(self):
"test to_native_str()"
from passlib.utils import to_native_str
# test plain ascii
self.assertEqual(to_native_str(u('abc'), 'ascii'), 'abc')
self.assertEqual(to_native_str(b('abc'), 'ascii'), 'abc')
# test invalid ascii
if PY3:
self.assertEqual(to_native_str(u('\xE0'), 'ascii'), '\xE0')
self.assertRaises(UnicodeDecodeError, to_native_str, b('\xC3\xA0'),
'ascii')
else:
self.assertRaises(UnicodeEncodeError, to_native_str, u('\xE0'),
'ascii')
self.assertEqual(to_native_str(b('\xC3\xA0'), 'ascii'), '\xC3\xA0')
# test latin-1
self.assertEqual(to_native_str(u('\xE0'), 'latin-1'), '\xE0')
self.assertEqual(to_native_str(b('\xE0'), 'latin-1'), '\xE0')
# test utf-8
self.assertEqual(to_native_str(u('\xE0'), 'utf-8'),
'\xE0' if PY3 else '\xC3\xA0')
self.assertEqual(to_native_str(b('\xC3\xA0'), 'utf-8'),
'\xE0' if PY3 else '\xC3\xA0')
# other types rejected
self.assertRaises(TypeError, to_native_str, None, 'ascii')
def test_is_ascii_safe(self):
"test is_ascii_safe()"
from passlib.utils import is_ascii_safe
self.assertTrue(is_ascii_safe(b("\x00abc\x7f")))
self.assertTrue(is_ascii_safe(u("\x00abc\x7f")))
self.assertFalse(is_ascii_safe(b("\x00abc\x80")))
self.assertFalse(is_ascii_safe(u("\x00abc\x80")))
def test_is_same_codec(self):
"test is_same_codec()"
from passlib.utils import is_same_codec
self.assertTrue(is_same_codec(None, None))
self.assertFalse(is_same_codec(None, 'ascii'))
self.assertTrue(is_same_codec("ascii", "ascii"))
self.assertTrue(is_same_codec("ascii", "ASCII"))
self.assertTrue(is_same_codec("utf-8", "utf-8"))
self.assertTrue(is_same_codec("utf-8", "utf8"))
self.assertTrue(is_same_codec("utf-8", "UTF_8"))
self.assertFalse(is_same_codec("ascii", "utf-8"))
#=============================================================================
# base64engine
#=============================================================================
class Base64EngineTest(TestCase):
"test standalone parts of Base64Engine"
# NOTE: most Base64Engine testing done via _Base64Test subclasses below.
def test_constructor(self):
from passlib.utils import Base64Engine, AB64_CHARS
# bad charmap type
self.assertRaises(TypeError, Base64Engine, 1)
# bad charmap size
self.assertRaises(ValueError, Base64Engine, AB64_CHARS[:-1])
# dup charmap letter
self.assertRaises(ValueError, Base64Engine, AB64_CHARS[:-1] + "A")
def test_ab64(self):
from passlib.utils import ab64_decode
# TODO: make ab64_decode (and a b64 variant) *much* stricter about
# padding chars, etc.
# 1 mod 4 not valid
self.assertRaises(ValueError, ab64_decode, "abcde")
class _Base64Test(TestCase):
"common tests for all Base64Engine instances"
#===================================================================
# class attrs
#===================================================================
# Base64Engine instance to test
engine = None
# pairs of (raw, encoded) bytes to test - should encode/decode correctly
encoded_data = None
# tuples of (encoded, value, bits) for known integer encodings
encoded_ints = None
# invalid encoded byte
bad_byte = b("?")
# helper to generate bytemap-specific strings
def m(self, *offsets):
"generate byte string from offsets"
return join_bytes(self.engine.bytemap[o:o+1] for o in offsets)
#===================================================================
# test encode_bytes
#===================================================================
def test_encode_bytes(self):
"test encode_bytes() against reference inputs"
engine = self.engine
encode = engine.encode_bytes
for raw, encoded in self.encoded_data:
result = encode(raw)
self.assertEqual(result, encoded, "encode %r:" % (raw,))
def test_encode_bytes_bad(self):
"test encode_bytes() with bad input"
engine = self.engine
encode = engine.encode_bytes
self.assertRaises(TypeError, encode, u('\x00'))
self.assertRaises(TypeError, encode, None)
#===================================================================
# test decode_bytes
#===================================================================
def test_decode_bytes(self):
"test decode_bytes() against reference inputs"
engine = self.engine
decode = engine.decode_bytes
for raw, encoded in self.encoded_data:
result = decode(encoded)
self.assertEqual(result, raw, "decode %r:" % (encoded,))
def test_decode_bytes_padding(self):
"test decode_bytes() ignores padding bits"
bchr = (lambda v: bytes([v])) if PY3 else chr
engine = self.engine
m = self.m
decode = engine.decode_bytes
BNULL = b("\x00")
# length == 2 mod 4: 4 bits of padding
self.assertEqual(decode(m(0,0)), BNULL)
for i in range(0,6):
if engine.big: # 4 lsb padding
correct = BNULL if i < 4 else bchr(1<<(i-4))
else: # 4 msb padding
correct = bchr(1<<(i+6)) if i < 2 else BNULL
self.assertEqual(decode(m(0,1<<i)), correct, "%d/4 bits:" % i)
# length == 3 mod 4: 2 bits of padding
self.assertEqual(decode(m(0,0,0)), BNULL*2)
for i in range(0,6):
if engine.big: # 2 lsb are padding
correct = BNULL if i < 2 else bchr(1<<(i-2))
else: # 2 msg are padding
correct = bchr(1<<(i+4)) if i < 4 else BNULL
self.assertEqual(decode(m(0,0,1<<i)), BNULL + correct,
"%d/2 bits:" % i)
def test_decode_bytes_bad(self):
"test decode_bytes() with bad input"
engine = self.engine
decode = engine.decode_bytes
# wrong size (1 % 4)
self.assertRaises(ValueError, decode, engine.bytemap[:5])
# wrong char
self.assertTrue(self.bad_byte not in engine.bytemap)
self.assertRaises(ValueError, decode, self.bad_byte*4)
# wrong type
self.assertRaises(TypeError, decode, engine.charmap[:4])
self.assertRaises(TypeError, decode, None)
#===================================================================
# encode_bytes+decode_bytes
#===================================================================
def test_codec(self):
"test encode_bytes/decode_bytes against random data"
engine = self.engine
from passlib.utils import getrandbytes, getrandstr
saw_zero = False
for i in irange(500):
#
# test raw -> encode() -> decode() -> raw
#
# generate some random bytes
size = random.randint(1 if saw_zero else 0, 12)
if not size:
saw_zero = True
enc_size = (4*size+2)//3
raw = getrandbytes(random, size)
# encode them, check invariants
encoded = engine.encode_bytes(raw)
self.assertEqual(len(encoded), enc_size)
# make sure decode returns original
result = engine.decode_bytes(encoded)
self.assertEqual(result, raw)
#
# test encoded -> decode() -> encode() -> encoded
#
# generate some random encoded data
if size % 4 == 1:
size += random.choice([-1,1,2])
raw_size = 3*size//4
encoded = getrandstr(random, engine.bytemap, size)
# decode them, check invariants
raw = engine.decode_bytes(encoded)
self.assertEqual(len(raw), raw_size, "encoded %d:" % size)
# make sure encode returns original (barring padding bits)
result = engine.encode_bytes(raw)
if size % 4:
self.assertEqual(result[:-1], encoded[:-1])
else:
self.assertEqual(result, encoded)
def test_repair_unused(self):
"test repair_unused()"
# NOTE: this test relies on encode_bytes() always returning clear
# padding bits - which should be ensured by test vectors.
from passlib.utils import rng, getrandstr
engine = self.engine
check_repair_unused = self.engine.check_repair_unused
i = 0
while i < 300:
size = rng.randint(0,23)
cdata = getrandstr(rng, engine.charmap, size).encode("ascii")
if size & 3 == 1:
# should throw error
self.assertRaises(ValueError, check_repair_unused, cdata)
continue
rdata = engine.encode_bytes(engine.decode_bytes(cdata))
if rng.random() < .5:
cdata = cdata.decode("ascii")
rdata = rdata.decode("ascii")
if cdata == rdata:
# should leave unchanged
ok, result = check_repair_unused(cdata)
self.assertFalse(ok)
self.assertEqual(result, rdata)
else:
# should repair bits
self.assertNotEqual(size % 4, 0)
ok, result = check_repair_unused(cdata)
self.assertTrue(ok)
self.assertEqual(result, rdata)
i += 1
#===================================================================
# test transposed encode/decode - encoding independant
#===================================================================
# NOTE: these tests assume normal encode/decode has been tested elsewhere.
transposed = [
# orig, result, transpose map
(b("\x33\x22\x11"), b("\x11\x22\x33"),[2,1,0]),
(b("\x22\x33\x11"), b("\x11\x22\x33"),[1,2,0]),
]
transposed_dups = [
# orig, result, transpose projection
(b("\x11\x11\x22"), b("\x11\x22\x33"),[0,0,1]),
]
def test_encode_transposed_bytes(self):
"test encode_transposed_bytes()"
engine = self.engine
for result, input, offsets in self.transposed + self.transposed_dups:
tmp = engine.encode_transposed_bytes(input, offsets)
out = engine.decode_bytes(tmp)
self.assertEqual(out, result)
self.assertRaises(TypeError, engine.encode_transposed_bytes, u("a"), [])
def test_decode_transposed_bytes(self):
"test decode_transposed_bytes()"
engine = self.engine
for input, result, offsets in self.transposed:
tmp = engine.encode_bytes(input)
out = engine.decode_transposed_bytes(tmp, offsets)
self.assertEqual(out, result)
def test_decode_transposed_bytes_bad(self):
"test decode_transposed_bytes() fails if map is a one-way"
engine = self.engine
for input, _, offsets in self.transposed_dups:
tmp = engine.encode_bytes(input)
self.assertRaises(TypeError, engine.decode_transposed_bytes, tmp,
offsets)
#===================================================================
# test 6bit handling
#===================================================================
def check_int_pair(self, bits, encoded_pairs):
"helper to check encode_intXX & decode_intXX functions"
engine = self.engine
encode = getattr(engine, "encode_int%s" % bits)
decode = getattr(engine, "decode_int%s" % bits)
pad = -bits % 6
chars = (bits+pad)//6
upper = 1<<bits
# test encode func
for value, encoded in encoded_pairs:
result = encode(value)
self.assertIsInstance(result, bytes)
self.assertEqual(result, encoded)
self.assertRaises(ValueError, encode, -1)
self.assertRaises(ValueError, encode, upper)
# test decode func
for value, encoded in encoded_pairs:
self.assertEqual(decode(encoded), value, "encoded %r:" % (encoded,))
m = self.m
self.assertRaises(ValueError, decode, m(0)*(chars+1))
self.assertRaises(ValueError, decode, m(0)*(chars-1))
self.assertRaises(ValueError, decode, self.bad_byte*chars)
self.assertRaises(TypeError, decode, engine.charmap[0])
self.assertRaises(TypeError, decode, None)
# do random testing.
from passlib.utils import getrandbytes, getrandstr
for i in irange(100):
# generate random value, encode, and then decode
value = random.randint(0, upper-1)
encoded = encode(value)
self.assertEqual(len(encoded), chars)
self.assertEqual(decode(encoded), value)
# generate some random encoded data, decode, then encode.
encoded = getrandstr(random, engine.bytemap, chars)
value = decode(encoded)
self.assertGreaterEqual(value, 0, "decode %r out of bounds:" % encoded)
self.assertLess(value, upper, "decode %r out of bounds:" % encoded)
result = encode(value)
if pad:
self.assertEqual(result[:-2], encoded[:-2])
else:
self.assertEqual(result, encoded)
def test_int6(self):
engine = self.engine
m = self.m
self.check_int_pair(6, [(0, m(0)), (63, m(63))])
def test_int12(self):
engine = self.engine
m = self.m
self.check_int_pair(12,[(0, m(0,0)),
(63, m(0,63) if engine.big else m(63,0)), (0xFFF, m(63,63))])
def test_int24(self):
engine = self.engine
m = self.m
self.check_int_pair(24,[(0, m(0,0,0,0)),
(63, m(0,0,0,63) if engine.big else m(63,0,0,0)),
(0xFFFFFF, m(63,63,63,63))])
def test_int64(self):
# NOTE: this isn't multiple of 6, it has 2 padding bits appended
# before encoding.
engine = self.engine
m = self.m
self.check_int_pair(64, [(0, m(0,0,0,0, 0,0,0,0, 0,0,0)),
(63, m(0,0,0,0, 0,0,0,0, 0,3,60) if engine.big else
m(63,0,0,0, 0,0,0,0, 0,0,0)),
((1<<64)-1, m(63,63,63,63, 63,63,63,63, 63,63,60) if engine.big
else m(63,63,63,63, 63,63,63,63, 63,63,15))])
def test_encoded_ints(self):
"test against reference integer encodings"
if not self.encoded_ints:
raise self.skipTests("none defined for class")
engine = self.engine
for data, value, bits in self.encoded_ints:
encode = getattr(engine, "encode_int%d" % bits)
decode = getattr(engine, "decode_int%d" % bits)
self.assertEqual(encode(value), data)
self.assertEqual(decode(data), value)
#===================================================================
# eoc
#===================================================================
# NOTE: testing H64 & H64Big should be sufficient to verify
# that Base64Engine() works in general.
from passlib.utils import h64, h64big
class H64_Test(_Base64Test):
"test H64 codec functions"
engine = h64
descriptionPrefix = "h64 codec"
encoded_data = [
# test lengths 0..6 to ensure tail is encoded properly
(b(""),b("")),
(b("\x55"),b("J/")),
(b("\x55\xaa"),b("Jd8")),
(b("\x55\xaa\x55"),b("JdOJ")),
(b("\x55\xaa\x55\xaa"),b("JdOJe0")),
(b("\x55\xaa\x55\xaa\x55"),b("JdOJeK3")),
(b("\x55\xaa\x55\xaa\x55\xaa"),b("JdOJeKZe")),
# test padding bits are null
(b("\x55\xaa\x55\xaf"),b("JdOJj0")), # len = 1 mod 3
(b("\x55\xaa\x55\xaa\x5f"),b("JdOJey3")), # len = 2 mod 3
]
encoded_ints = [
(b("z."), 63, 12),
(b(".z"), 4032, 12),
]
class H64Big_Test(_Base64Test):
"test H64Big codec functions"
engine = h64big
descriptionPrefix = "h64big codec"
encoded_data = [
# test lengths 0..6 to ensure tail is encoded properly
(b(""),b("")),
(b("\x55"),b("JE")),
(b("\x55\xaa"),b("JOc")),
(b("\x55\xaa\x55"),b("JOdJ")),
(b("\x55\xaa\x55\xaa"),b("JOdJeU")),
(b("\x55\xaa\x55\xaa\x55"),b("JOdJeZI")),
(b("\x55\xaa\x55\xaa\x55\xaa"),b("JOdJeZKe")),
# test padding bits are null
(b("\x55\xaa\x55\xaf"),b("JOdJfk")), # len = 1 mod 3
(b("\x55\xaa\x55\xaa\x5f"),b("JOdJeZw")), # len = 2 mod 3
]
encoded_ints = [
(b(".z"), 63, 12),
(b("z."), 4032, 12),
]
#=============================================================================
# eof
#=============================================================================

View File

@ -0,0 +1,599 @@
"""tests for passlib.utils.(des|pbkdf2|md4)"""
#=============================================================================
# imports
#=============================================================================
from __future__ import with_statement
# core
from binascii import hexlify, unhexlify
import hashlib
import hmac
import sys
import random
import warnings
# site
try:
import M2Crypto
except ImportError:
M2Crypto = None
# pkg
# module
from passlib.utils.compat import b, bytes, bascii_to_str, irange, PY2, PY3, u, \
unicode, join_bytes, PYPY, JYTHON
from passlib.tests.utils import TestCase, TEST_MODE, catch_warnings, skipUnless, skipIf
#=============================================================================
# support
#=============================================================================
def hb(source):
return unhexlify(b(source))
#=============================================================================
# test assorted crypto helpers
#=============================================================================
class CryptoTest(TestCase):
"test various crypto functions"
ndn_formats = ["hashlib", "iana"]
ndn_values = [
# (iana name, hashlib name, ... other unnormalized names)
("md5", "md5", "SCRAM-MD5-PLUS", "MD-5"),
("sha1", "sha-1", "SCRAM-SHA-1", "SHA1"),
("sha256", "sha-256", "SHA_256", "sha2-256"),
("ripemd", "ripemd", "SCRAM-RIPEMD", "RIPEMD"),
("ripemd160", "ripemd-160",
"SCRAM-RIPEMD-160", "RIPEmd160"),
("test128", "test-128", "TEST128"),
("test2", "test2", "TEST-2"),
("test3128", "test3-128", "TEST-3-128"),
]
def test_norm_hash_name(self):
"test norm_hash_name()"
from itertools import chain
from passlib.utils.pbkdf2 import norm_hash_name, _nhn_hash_names
# test formats
for format in self.ndn_formats:
norm_hash_name("md4", format)
self.assertRaises(ValueError, norm_hash_name, "md4", None)
self.assertRaises(ValueError, norm_hash_name, "md4", "fake")
# test types
self.assertEqual(norm_hash_name(u("MD4")), "md4")
self.assertEqual(norm_hash_name(b("MD4")), "md4")
self.assertRaises(TypeError, norm_hash_name, None)
# test selected results
with catch_warnings():
warnings.filterwarnings("ignore", '.*unknown hash')
for row in chain(_nhn_hash_names, self.ndn_values):
for idx, format in enumerate(self.ndn_formats):
correct = row[idx]
for value in row:
result = norm_hash_name(value, format)
self.assertEqual(result, correct,
"name=%r, format=%r:" % (value,
format))
# TODO: write full test of get_prf(), currently relying on pbkdf2 testing
#=============================================================================
# test DES routines
#=============================================================================
class DesTest(TestCase):
descriptionPrefix = "DES"
# test vectors taken from http://www.skepticfiles.org/faq/testdes.htm
des_test_vectors = [
# key, plaintext, ciphertext
(0x0000000000000000, 0x0000000000000000, 0x8CA64DE9C1B123A7),
(0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0x7359B2163E4EDC58),
(0x3000000000000000, 0x1000000000000001, 0x958E6E627A05557B),
(0x1111111111111111, 0x1111111111111111, 0xF40379AB9E0EC533),
(0x0123456789ABCDEF, 0x1111111111111111, 0x17668DFC7292532D),
(0x1111111111111111, 0x0123456789ABCDEF, 0x8A5AE1F81AB8F2DD),
(0x0000000000000000, 0x0000000000000000, 0x8CA64DE9C1B123A7),
(0xFEDCBA9876543210, 0x0123456789ABCDEF, 0xED39D950FA74BCC4),
(0x7CA110454A1A6E57, 0x01A1D6D039776742, 0x690F5B0D9A26939B),
(0x0131D9619DC1376E, 0x5CD54CA83DEF57DA, 0x7A389D10354BD271),
(0x07A1133E4A0B2686, 0x0248D43806F67172, 0x868EBB51CAB4599A),
(0x3849674C2602319E, 0x51454B582DDF440A, 0x7178876E01F19B2A),
(0x04B915BA43FEB5B6, 0x42FD443059577FA2, 0xAF37FB421F8C4095),
(0x0113B970FD34F2CE, 0x059B5E0851CF143A, 0x86A560F10EC6D85B),
(0x0170F175468FB5E6, 0x0756D8E0774761D2, 0x0CD3DA020021DC09),
(0x43297FAD38E373FE, 0x762514B829BF486A, 0xEA676B2CB7DB2B7A),
(0x07A7137045DA2A16, 0x3BDD119049372802, 0xDFD64A815CAF1A0F),
(0x04689104C2FD3B2F, 0x26955F6835AF609A, 0x5C513C9C4886C088),
(0x37D06BB516CB7546, 0x164D5E404F275232, 0x0A2AEEAE3FF4AB77),
(0x1F08260D1AC2465E, 0x6B056E18759F5CCA, 0xEF1BF03E5DFA575A),
(0x584023641ABA6176, 0x004BD6EF09176062, 0x88BF0DB6D70DEE56),
(0x025816164629B007, 0x480D39006EE762F2, 0xA1F9915541020B56),
(0x49793EBC79B3258F, 0x437540C8698F3CFA, 0x6FBF1CAFCFFD0556),
(0x4FB05E1515AB73A7, 0x072D43A077075292, 0x2F22E49BAB7CA1AC),
(0x49E95D6D4CA229BF, 0x02FE55778117F12A, 0x5A6B612CC26CCE4A),
(0x018310DC409B26D6, 0x1D9D5C5018F728C2, 0x5F4C038ED12B2E41),
(0x1C587F1C13924FEF, 0x305532286D6F295A, 0x63FAC0D034D9F793),
(0x0101010101010101, 0x0123456789ABCDEF, 0x617B3A0CE8F07100),
(0x1F1F1F1F0E0E0E0E, 0x0123456789ABCDEF, 0xDB958605F8C8C606),
(0xE0FEE0FEF1FEF1FE, 0x0123456789ABCDEF, 0xEDBFD1C66C29CCC7),
(0x0000000000000000, 0xFFFFFFFFFFFFFFFF, 0x355550B2150E2451),
(0xFFFFFFFFFFFFFFFF, 0x0000000000000000, 0xCAAAAF4DEAF1DBAE),
(0x0123456789ABCDEF, 0x0000000000000000, 0xD5D44FF720683D0D),
(0xFEDCBA9876543210, 0xFFFFFFFFFFFFFFFF, 0x2A2BB008DF97C2F2),
]
def test_01_expand(self):
"test expand_des_key()"
from passlib.utils.des import expand_des_key, shrink_des_key, \
_KDATA_MASK, INT_56_MASK
# make sure test vectors are preserved (sans parity bits)
# uses ints, bytes are tested under # 02
for key1, _, _ in self.des_test_vectors:
key2 = shrink_des_key(key1)
key3 = expand_des_key(key2)
# NOTE: this assumes expand_des_key() sets parity bits to 0
self.assertEqual(key3, key1 & _KDATA_MASK)
# type checks
self.assertRaises(TypeError, expand_des_key, 1.0)
# too large
self.assertRaises(ValueError, expand_des_key, INT_56_MASK+1)
self.assertRaises(ValueError, expand_des_key, b("\x00")*8)
# too small
self.assertRaises(ValueError, expand_des_key, -1)
self.assertRaises(ValueError, expand_des_key, b("\x00")*6)
def test_02_shrink(self):
"test shrink_des_key()"
from passlib.utils.des import expand_des_key, shrink_des_key, \
INT_64_MASK
from passlib.utils import random, getrandbytes
# make sure reverse works for some random keys
# uses bytes, ints are tested under # 01
for i in range(20):
key1 = getrandbytes(random, 7)
key2 = expand_des_key(key1)
key3 = shrink_des_key(key2)
self.assertEqual(key3, key1)
# type checks
self.assertRaises(TypeError, shrink_des_key, 1.0)
# too large
self.assertRaises(ValueError, shrink_des_key, INT_64_MASK+1)
self.assertRaises(ValueError, shrink_des_key, b("\x00")*9)
# too small
self.assertRaises(ValueError, shrink_des_key, -1)
self.assertRaises(ValueError, shrink_des_key, b("\x00")*7)
def _random_parity(self, key):
"randomize parity bits"
from passlib.utils.des import _KDATA_MASK, _KPARITY_MASK, INT_64_MASK
from passlib.utils import rng
return (key & _KDATA_MASK) | (rng.randint(0,INT_64_MASK) & _KPARITY_MASK)
def test_03_encrypt_bytes(self):
"test des_encrypt_block()"
from passlib.utils.des import (des_encrypt_block, shrink_des_key,
_pack64, _unpack64)
# run through test vectors
for key, plaintext, correct in self.des_test_vectors:
# convert to bytes
key = _pack64(key)
plaintext = _pack64(plaintext)
correct = _pack64(correct)
# test 64-bit key
result = des_encrypt_block(key, plaintext)
self.assertEqual(result, correct, "key=%r plaintext=%r:" %
(key, plaintext))
# test 56-bit version
key2 = shrink_des_key(key)
result = des_encrypt_block(key2, plaintext)
self.assertEqual(result, correct, "key=%r shrink(key)=%r plaintext=%r:" %
(key, key2, plaintext))
# test with random parity bits
for _ in range(20):
key3 = _pack64(self._random_parity(_unpack64(key)))
result = des_encrypt_block(key3, plaintext)
self.assertEqual(result, correct, "key=%r rndparity(key)=%r plaintext=%r:" %
(key, key3, plaintext))
# check invalid keys
stub = b('\x00') * 8
self.assertRaises(TypeError, des_encrypt_block, 0, stub)
self.assertRaises(ValueError, des_encrypt_block, b('\x00')*6, stub)
# check invalid input
self.assertRaises(TypeError, des_encrypt_block, stub, 0)
self.assertRaises(ValueError, des_encrypt_block, stub, b('\x00')*7)
# check invalid salts
self.assertRaises(ValueError, des_encrypt_block, stub, stub, salt=-1)
self.assertRaises(ValueError, des_encrypt_block, stub, stub, salt=1<<24)
# check invalid rounds
self.assertRaises(ValueError, des_encrypt_block, stub, stub, 0, rounds=0)
def test_04_encrypt_ints(self):
"test des_encrypt_int_block()"
from passlib.utils.des import (des_encrypt_int_block, shrink_des_key)
# run through test vectors
for key, plaintext, correct in self.des_test_vectors:
# test 64-bit key
result = des_encrypt_int_block(key, plaintext)
self.assertEqual(result, correct, "key=%r plaintext=%r:" %
(key, plaintext))
# test with random parity bits
for _ in range(20):
key3 = self._random_parity(key)
result = des_encrypt_int_block(key3, plaintext)
self.assertEqual(result, correct, "key=%r rndparity(key)=%r plaintext=%r:" %
(key, key3, plaintext))
# check invalid keys
self.assertRaises(TypeError, des_encrypt_int_block, b('\x00'), 0)
self.assertRaises(ValueError, des_encrypt_int_block, -1, 0)
# check invalid input
self.assertRaises(TypeError, des_encrypt_int_block, 0, b('\x00'))
self.assertRaises(ValueError, des_encrypt_int_block, 0, -1)
# check invalid salts
self.assertRaises(ValueError, des_encrypt_int_block, 0, 0, salt=-1)
self.assertRaises(ValueError, des_encrypt_int_block, 0, 0, salt=1<<24)
# check invalid rounds
self.assertRaises(ValueError, des_encrypt_int_block, 0, 0, 0, rounds=0)
#=============================================================================
# test pure-python MD4 implementation
#=============================================================================
from passlib.utils.md4 import _has_native_md4
has_native_md4 = _has_native_md4()
class _MD4_Test(TestCase):
_disable_native = False
def setUp(self):
super(_MD4_Test, self).setUp()
import passlib.utils.md4 as mod
if has_native_md4 and self._disable_native:
self.addCleanup(setattr, mod, "md4", mod.md4)
mod.md4 = mod._builtin_md4
vectors = [
# input -> hex digest
# test vectors from http://www.faqs.org/rfcs/rfc1320.html - A.5
(b(""), "31d6cfe0d16ae931b73c59d7e0c089c0"),
(b("a"), "bde52cb31de33e46245e05fbdbd6fb24"),
(b("abc"), "a448017aaf21d8525fc10ae87aa6729d"),
(b("message digest"), "d9130a8164549fe818874806e1c7014b"),
(b("abcdefghijklmnopqrstuvwxyz"), "d79e1c308aa5bbcdeea8ed63df412da9"),
(b("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"), "043f8582f241db351ce627e153e7f0e4"),
(b("12345678901234567890123456789012345678901234567890123456789012345678901234567890"), "e33b4ddc9c38f2199c3e7b164fcc0536"),
]
def test_md4_update(self):
"test md4 update"
from passlib.utils.md4 import md4
h = md4(b(''))
self.assertEqual(h.hexdigest(), "31d6cfe0d16ae931b73c59d7e0c089c0")
# NOTE: under py2, hashlib methods try to encode to ascii,
# though shouldn't rely on that.
if PY3 or self._disable_native:
self.assertRaises(TypeError, h.update, u('x'))
h.update(b('a'))
self.assertEqual(h.hexdigest(), "bde52cb31de33e46245e05fbdbd6fb24")
h.update(b('bcdefghijklmnopqrstuvwxyz'))
self.assertEqual(h.hexdigest(), "d79e1c308aa5bbcdeea8ed63df412da9")
def test_md4_hexdigest(self):
"test md4 hexdigest()"
from passlib.utils.md4 import md4
for input, hex in self.vectors:
out = md4(input).hexdigest()
self.assertEqual(out, hex)
def test_md4_digest(self):
"test md4 digest()"
from passlib.utils.md4 import md4
for input, hex in self.vectors:
out = bascii_to_str(hexlify(md4(input).digest()))
self.assertEqual(out, hex)
def test_md4_copy(self):
"test md4 copy()"
from passlib.utils.md4 import md4
h = md4(b('abc'))
h2 = h.copy()
h2.update(b('def'))
self.assertEqual(h2.hexdigest(), '804e7f1c2586e50b49ac65db5b645131')
h.update(b('ghi'))
self.assertEqual(h.hexdigest(), 'c5225580bfe176f6deeee33dee98732c')
# create subclasses to test with and without native backend
class MD4_SSL_Test(_MD4_Test):
descriptionPrefix = "MD4 (ssl version)"
MD4_SSL_TEST = skipUnless(has_native_md4, "hashlib lacks ssl support")(MD4_SSL_Test)
class MD4_Builtin_Test(_MD4_Test):
descriptionPrefix = "MD4 (builtin version)"
_disable_native = True
MD4_Builtin_Test = skipUnless(TEST_MODE("full") or not has_native_md4,
"skipped under current test mode")(MD4_Builtin_Test)
#=============================================================================
# test PBKDF1 support
#=============================================================================
class Pbkdf1_Test(TestCase):
"test kdf helpers"
descriptionPrefix = "pbkdf1"
pbkdf1_tests = [
# (password, salt, rounds, keylen, hash, result)
#
# from http://www.di-mgt.com.au/cryptoKDFs.html
#
(b('password'), hb('78578E5A5D63CB06'), 1000, 16, 'sha1', hb('dc19847e05c64d2faf10ebfb4a3d2a20')),
#
# custom
#
(b('password'), b('salt'), 1000, 0, 'md5', b('')),
(b('password'), b('salt'), 1000, 1, 'md5', hb('84')),
(b('password'), b('salt'), 1000, 8, 'md5', hb('8475c6a8531a5d27')),
(b('password'), b('salt'), 1000, 16, 'md5', hb('8475c6a8531a5d27e386cd496457812c')),
(b('password'), b('salt'), 1000, None, 'md5', hb('8475c6a8531a5d27e386cd496457812c')),
(b('password'), b('salt'), 1000, None, 'sha1', hb('4a8fd48e426ed081b535be5769892fa396293efb')),
]
if not (PYPY or JYTHON):
pbkdf1_tests.append(
(b('password'), b('salt'), 1000, None, 'md4', hb('f7f2e91100a8f96190f2dd177cb26453'))
)
def test_known(self):
"test reference vectors"
from passlib.utils.pbkdf2 import pbkdf1
for secret, salt, rounds, keylen, digest, correct in self.pbkdf1_tests:
result = pbkdf1(secret, salt, rounds, keylen, digest)
self.assertEqual(result, correct)
def test_border(self):
"test border cases"
from passlib.utils.pbkdf2 import pbkdf1
def helper(secret=b('secret'), salt=b('salt'), rounds=1, keylen=1, hash='md5'):
return pbkdf1(secret, salt, rounds, keylen, hash)
helper()
# salt/secret wrong type
self.assertRaises(TypeError, helper, secret=1)
self.assertRaises(TypeError, helper, salt=1)
# non-existent hashes
self.assertRaises(ValueError, helper, hash='missing')
# rounds < 1 and wrong type
self.assertRaises(ValueError, helper, rounds=0)
self.assertRaises(TypeError, helper, rounds='1')
# keylen < 0, keylen > block_size, and wrong type
self.assertRaises(ValueError, helper, keylen=-1)
self.assertRaises(ValueError, helper, keylen=17, hash='md5')
self.assertRaises(TypeError, helper, keylen='1')
#=============================================================================
# test PBKDF2 support
#=============================================================================
class _Pbkdf2_Test(TestCase):
"test pbkdf2() support"
_disable_m2crypto = False
def setUp(self):
super(_Pbkdf2_Test, self).setUp()
import passlib.utils.pbkdf2 as mod
# disable m2crypto support, and use software backend
if M2Crypto and self._disable_m2crypto:
self.addCleanup(setattr, mod, "_EVP", mod._EVP)
mod._EVP = None
# flush cached prf functions, since we're screwing with their backend.
mod._clear_prf_cache()
self.addCleanup(mod._clear_prf_cache)
pbkdf2_test_vectors = [
# (result, secret, salt, rounds, keylen, prf="sha1")
#
# from rfc 3962
#
# test case 1 / 128 bit
(
hb("cdedb5281bb2f801565a1122b2563515"),
b("password"), b("ATHENA.MIT.EDUraeburn"), 1, 16
),
# test case 2 / 128 bit
(
hb("01dbee7f4a9e243e988b62c73cda935d"),
b("password"), b("ATHENA.MIT.EDUraeburn"), 2, 16
),
# test case 2 / 256 bit
(
hb("01dbee7f4a9e243e988b62c73cda935da05378b93244ec8f48a99e61ad799d86"),
b("password"), b("ATHENA.MIT.EDUraeburn"), 2, 32
),
# test case 3 / 256 bit
(
hb("5c08eb61fdf71e4e4ec3cf6ba1f5512ba7e52ddbc5e5142f708a31e2e62b1e13"),
b("password"), b("ATHENA.MIT.EDUraeburn"), 1200, 32
),
# test case 4 / 256 bit
(
hb("d1daa78615f287e6a1c8b120d7062a493f98d203e6be49a6adf4fa574b6e64ee"),
b("password"), b('\x12\x34\x56\x78\x78\x56\x34\x12'), 5, 32
),
# test case 5 / 256 bit
(
hb("139c30c0966bc32ba55fdbf212530ac9c5ec59f1a452f5cc9ad940fea0598ed1"),
b("X"*64), b("pass phrase equals block size"), 1200, 32
),
# test case 6 / 256 bit
(
hb("9ccad6d468770cd51b10e6a68721be611a8b4d282601db3b36be9246915ec82a"),
b("X"*65), b("pass phrase exceeds block size"), 1200, 32
),
#
# from rfc 6070
#
(
hb("0c60c80f961f0e71f3a9b524af6012062fe037a6"),
b("password"), b("salt"), 1, 20,
),
(
hb("ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957"),
b("password"), b("salt"), 2, 20,
),
(
hb("4b007901b765489abead49d926f721d065a429c1"),
b("password"), b("salt"), 4096, 20,
),
# just runs too long - could enable if ALL option is set
##(
##
## unhexlify("eefe3d61cd4da4e4e9945b3d6ba2158c2634e984"),
## "password", "salt", 16777216, 20,
##),
(
hb("3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038"),
b("passwordPASSWORDpassword"),
b("saltSALTsaltSALTsaltSALTsaltSALTsalt"),
4096, 25,
),
(
hb("56fa6aa75548099dcc37d7f03425e0c3"),
b("pass\00word"), b("sa\00lt"), 4096, 16,
),
#
# from example in http://grub.enbug.org/Authentication
#
(
hb("887CFF169EA8335235D8004242AA7D6187A41E3187DF0CE14E256D85ED"
"97A97357AAA8FF0A3871AB9EEFF458392F462F495487387F685B7472FC"
"6C29E293F0A0"),
b("hello"),
hb("9290F727ED06C38BA4549EF7DE25CF5642659211B7FC076F2D28FEFD71"
"784BB8D8F6FB244A8CC5C06240631B97008565A120764C0EE9C2CB0073"
"994D79080136"),
10000, 64, "hmac-sha512"
),
#
# custom
#
(
hb('e248fb6b13365146f8ac6307cc222812'),
b("secret"), b("salt"), 10, 16, "hmac-sha1",
),
(
hb('e248fb6b13365146f8ac6307cc2228127872da6d'),
b("secret"), b("salt"), 10, None, "hmac-sha1",
),
]
def test_known(self):
"test reference vectors"
from passlib.utils.pbkdf2 import pbkdf2
for row in self.pbkdf2_test_vectors:
correct, secret, salt, rounds, keylen = row[:5]
prf = row[5] if len(row) == 6 else "hmac-sha1"
result = pbkdf2(secret, salt, rounds, keylen, prf)
self.assertEqual(result, correct)
def test_border(self):
"test border cases"
from passlib.utils.pbkdf2 import pbkdf2
def helper(secret=b('password'), salt=b('salt'), rounds=1, keylen=None, prf="hmac-sha1"):
return pbkdf2(secret, salt, rounds, keylen, prf)
helper()
# invalid rounds
self.assertRaises(ValueError, helper, rounds=0)
self.assertRaises(TypeError, helper, rounds='x')
# invalid keylen
helper(keylen=0)
self.assertRaises(ValueError, helper, keylen=-1)
self.assertRaises(ValueError, helper, keylen=20*(2**32-1)+1)
self.assertRaises(TypeError, helper, keylen='x')
# invalid secret/salt type
self.assertRaises(TypeError, helper, salt=5)
self.assertRaises(TypeError, helper, secret=5)
# invalid hash
self.assertRaises(ValueError, helper, prf='hmac-foo')
self.assertRaises(ValueError, helper, prf='foo')
self.assertRaises(TypeError, helper, prf=5)
def test_default_keylen(self):
"test keylen==None"
from passlib.utils.pbkdf2 import pbkdf2
def helper(secret=b('password'), salt=b('salt'), rounds=1, keylen=None, prf="hmac-sha1"):
return pbkdf2(secret, salt, rounds, keylen, prf)
self.assertEqual(len(helper(prf='hmac-sha1')), 20)
self.assertEqual(len(helper(prf='hmac-sha256')), 32)
def test_custom_prf(self):
"test custom prf function"
from passlib.utils.pbkdf2 import pbkdf2
def prf(key, msg):
return hashlib.md5(key+msg+b('fooey')).digest()
result = pbkdf2(b('secret'), b('salt'), 1000, 20, prf)
self.assertEqual(result, hb('5fe7ce9f7e379d3f65cbc66ba8aa6440474a6849'))
# create subclasses to test with and without m2crypto
class Pbkdf2_M2Crypto_Test(_Pbkdf2_Test):
descriptionPrefix = "pbkdf2 (m2crypto backend)"
Pbkdf2_M2Crypto_Test = skipUnless(M2Crypto, "M2Crypto not found")(Pbkdf2_M2Crypto_Test)
class Pbkdf2_Builtin_Test(_Pbkdf2_Test):
descriptionPrefix = "pbkdf2 (builtin backend)"
_disable_m2crypto = True
Pbkdf2_Builtin_Test = skipUnless(TEST_MODE("full") or not M2Crypto,
"skipped under current test mode")(Pbkdf2_Builtin_Test)
#=============================================================================
# eof
#=============================================================================

View File

@ -0,0 +1,806 @@
"""tests for passlib.pwhash -- (c) Assurance Technologies 2003-2009"""
#=============================================================================
# imports
#=============================================================================
from __future__ import with_statement
# core
import re
import hashlib
from logging import getLogger
import warnings
# site
# pkg
from passlib.hash import ldap_md5, sha256_crypt
from passlib.registry import _unload_handler_name as unload_handler_name, \
register_crypt_handler, get_crypt_handler
from passlib.exc import MissingBackendError, PasslibHashWarning
from passlib.utils import getrandstr, JYTHON, rng
from passlib.utils.compat import b, bytes, bascii_to_str, str_to_uascii, \
uascii_to_str, unicode, PY_MAX_25, SUPPORTS_DIR_METHOD
import passlib.utils.handlers as uh
from passlib.tests.utils import HandlerCase, TestCase, catch_warnings
from passlib.utils.compat import u, PY3
# module
log = getLogger(__name__)
#=============================================================================
# utils
#=============================================================================
def _makelang(alphabet, size):
"generate all strings of given size using alphabet"
def helper(size):
if size < 2:
for char in alphabet:
yield char
else:
for char in alphabet:
for tail in helper(size-1):
yield char+tail
return set(helper(size))
#=============================================================================
# test GenericHandler & associates mixin classes
#=============================================================================
class SkeletonTest(TestCase):
"test hash support classes"
#===================================================================
# StaticHandler
#===================================================================
def test_00_static_handler(self):
"test StaticHandler class"
class d1(uh.StaticHandler):
name = "d1"
context_kwds = ("flag",)
_hash_prefix = u("_")
checksum_chars = u("ab")
checksum_size = 1
def __init__(self, flag=False, **kwds):
super(d1, self).__init__(**kwds)
self.flag = flag
def _calc_checksum(self, secret):
return u('b') if self.flag else u('a')
# check default identify method
self.assertTrue(d1.identify(u('_a')))
self.assertTrue(d1.identify(b('_a')))
self.assertTrue(d1.identify(u('_b')))
self.assertFalse(d1.identify(u('_c')))
self.assertFalse(d1.identify(b('_c')))
self.assertFalse(d1.identify(u('a')))
self.assertFalse(d1.identify(u('b')))
self.assertFalse(d1.identify(u('c')))
self.assertRaises(TypeError, d1.identify, None)
self.assertRaises(TypeError, d1.identify, 1)
# check default genconfig method
self.assertIs(d1.genconfig(), None)
# check default verify method
self.assertTrue(d1.verify('s', b('_a')))
self.assertTrue(d1.verify('s',u('_a')))
self.assertFalse(d1.verify('s', b('_b')))
self.assertFalse(d1.verify('s',u('_b')))
self.assertTrue(d1.verify('s', b('_b'), flag=True))
self.assertRaises(ValueError, d1.verify, 's', b('_c'))
self.assertRaises(ValueError, d1.verify, 's', u('_c'))
# check default encrypt method
self.assertEqual(d1.encrypt('s'), '_a')
self.assertEqual(d1.encrypt('s', flag=True), '_b')
def test_01_calc_checksum_hack(self):
"test StaticHandler legacy attr"
# release 1.5 StaticHandler required genhash(),
# not _calc_checksum, be implemented. we have backward compat wrapper,
# this tests that it works.
class d1(uh.StaticHandler):
name = "d1"
@classmethod
def identify(self, hash):
if not hash or len(hash) != 40:
return False
try:
int(hash, 16)
except ValueError:
return False
return True
@classmethod
def genhash(cls, secret, hash):
if secret is None:
raise TypeError("no secret provided")
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
if hash is not None and not cls.identify(hash):
raise ValueError("invalid hash")
return hashlib.sha1(b("xyz") + secret).hexdigest()
@classmethod
def verify(cls, secret, hash):
if hash is None:
raise ValueError("no hash specified")
return cls.genhash(secret, hash) == hash.lower()
# encrypt should issue api warnings, but everything else should be fine.
with self.assertWarningList("d1.*should be updated.*_calc_checksum"):
hash = d1.encrypt("test")
self.assertEqual(hash, '7c622762588a0e5cc786ad0a143156f9fd38eea3')
self.assertTrue(d1.verify("test", hash))
self.assertFalse(d1.verify("xtest", hash))
# not defining genhash either, however, should cause NotImplementedError
del d1.genhash
self.assertRaises(NotImplementedError, d1.encrypt, 'test')
#===================================================================
# GenericHandler & mixins
#===================================================================
def test_10_identify(self):
"test GenericHandler.identify()"
class d1(uh.GenericHandler):
@classmethod
def from_string(cls, hash):
if isinstance(hash, bytes):
hash = hash.decode("ascii")
if hash == u('a'):
return cls(checksum=hash)
else:
raise ValueError
# check fallback
self.assertRaises(TypeError, d1.identify, None)
self.assertRaises(TypeError, d1.identify, 1)
self.assertFalse(d1.identify(''))
self.assertTrue(d1.identify('a'))
self.assertFalse(d1.identify('b'))
# check regexp
d1._hash_regex = re.compile(u('@.'))
self.assertRaises(TypeError, d1.identify, None)
self.assertRaises(TypeError, d1.identify, 1)
self.assertTrue(d1.identify('@a'))
self.assertFalse(d1.identify('a'))
del d1._hash_regex
# check ident-based
d1.ident = u('!')
self.assertRaises(TypeError, d1.identify, None)
self.assertRaises(TypeError, d1.identify, 1)
self.assertTrue(d1.identify('!a'))
self.assertFalse(d1.identify('a'))
del d1.ident
def test_11_norm_checksum(self):
"test GenericHandler checksum handling"
# setup helpers
class d1(uh.GenericHandler):
name = 'd1'
checksum_size = 4
checksum_chars = u('xz')
_stub_checksum = u('z')*4
def norm_checksum(*a, **k):
return d1(*a, **k).checksum
# too small
self.assertRaises(ValueError, norm_checksum, u('xxx'))
# right size
self.assertEqual(norm_checksum(u('xxxx')), u('xxxx'))
self.assertEqual(norm_checksum(u('xzxz')), u('xzxz'))
# too large
self.assertRaises(ValueError, norm_checksum, u('xxxxx'))
# wrong chars
self.assertRaises(ValueError, norm_checksum, u('xxyx'))
# wrong type
self.assertRaises(TypeError, norm_checksum, b('xxyx'))
# relaxed
with self.assertWarningList("checksum should be unicode"):
self.assertEqual(norm_checksum(b('xxzx'), relaxed=True), u('xxzx'))
self.assertRaises(TypeError, norm_checksum, 1, relaxed=True)
# test _stub_checksum behavior
self.assertIs(norm_checksum(u('zzzz')), None)
def test_12_norm_checksum_raw(self):
"test GenericHandler + HasRawChecksum mixin"
class d1(uh.HasRawChecksum, uh.GenericHandler):
name = 'd1'
checksum_size = 4
_stub_checksum = b('0')*4
def norm_checksum(*a, **k):
return d1(*a, **k).checksum
# test bytes
self.assertEqual(norm_checksum(b('1234')), b('1234'))
# test unicode
self.assertRaises(TypeError, norm_checksum, u('xxyx'))
self.assertRaises(TypeError, norm_checksum, u('xxyx'), relaxed=True)
# test _stub_checksum behavior
self.assertIs(norm_checksum(b('0')*4), None)
def test_20_norm_salt(self):
"test GenericHandler + HasSalt mixin"
# setup helpers
class d1(uh.HasSalt, uh.GenericHandler):
name = 'd1'
setting_kwds = ('salt',)
min_salt_size = 2
max_salt_size = 4
default_salt_size = 3
salt_chars = 'ab'
def norm_salt(**k):
return d1(**k).salt
def gen_salt(sz, **k):
return d1(use_defaults=True, salt_size=sz, **k).salt
salts2 = _makelang('ab', 2)
salts3 = _makelang('ab', 3)
salts4 = _makelang('ab', 4)
# check salt=None
self.assertRaises(TypeError, norm_salt)
self.assertRaises(TypeError, norm_salt, salt=None)
self.assertIn(norm_salt(use_defaults=True), salts3)
# check explicit salts
with catch_warnings(record=True) as wlog:
# check too-small salts
self.assertRaises(ValueError, norm_salt, salt='')
self.assertRaises(ValueError, norm_salt, salt='a')
self.consumeWarningList(wlog)
# check correct salts
self.assertEqual(norm_salt(salt='ab'), 'ab')
self.assertEqual(norm_salt(salt='aba'), 'aba')
self.assertEqual(norm_salt(salt='abba'), 'abba')
self.consumeWarningList(wlog)
# check too-large salts
self.assertRaises(ValueError, norm_salt, salt='aaaabb')
self.consumeWarningList(wlog)
self.assertEqual(norm_salt(salt='aaaabb', relaxed=True), 'aaaa')
self.consumeWarningList(wlog, PasslibHashWarning)
# check generated salts
with catch_warnings(record=True) as wlog:
# check too-small salt size
self.assertRaises(ValueError, gen_salt, 0)
self.assertRaises(ValueError, gen_salt, 1)
self.consumeWarningList(wlog)
# check correct salt size
self.assertIn(gen_salt(2), salts2)
self.assertIn(gen_salt(3), salts3)
self.assertIn(gen_salt(4), salts4)
self.consumeWarningList(wlog)
# check too-large salt size
self.assertRaises(ValueError, gen_salt, 5)
self.consumeWarningList(wlog)
self.assertIn(gen_salt(5, relaxed=True), salts4)
self.consumeWarningList(wlog, ["salt too large"])
# test with max_salt_size=None
del d1.max_salt_size
with self.assertWarningList([]):
self.assertEqual(len(gen_salt(None)), 3)
self.assertEqual(len(gen_salt(5)), 5)
# TODO: test HasRawSalt mixin
def test_30_norm_rounds(self):
"test GenericHandler + HasRounds mixin"
# setup helpers
class d1(uh.HasRounds, uh.GenericHandler):
name = 'd1'
setting_kwds = ('rounds',)
min_rounds = 1
max_rounds = 3
default_rounds = 2
def norm_rounds(**k):
return d1(**k).rounds
# check rounds=None
self.assertRaises(TypeError, norm_rounds)
self.assertRaises(TypeError, norm_rounds, rounds=None)
self.assertEqual(norm_rounds(use_defaults=True), 2)
# check rounds=non int
self.assertRaises(TypeError, norm_rounds, rounds=1.5)
# check explicit rounds
with catch_warnings(record=True) as wlog:
# too small
self.assertRaises(ValueError, norm_rounds, rounds=0)
self.consumeWarningList(wlog)
self.assertEqual(norm_rounds(rounds=0, relaxed=True), 1)
self.consumeWarningList(wlog, PasslibHashWarning)
# just right
self.assertEqual(norm_rounds(rounds=1), 1)
self.assertEqual(norm_rounds(rounds=2), 2)
self.assertEqual(norm_rounds(rounds=3), 3)
self.consumeWarningList(wlog)
# too large
self.assertRaises(ValueError, norm_rounds, rounds=4)
self.consumeWarningList(wlog)
self.assertEqual(norm_rounds(rounds=4, relaxed=True), 3)
self.consumeWarningList(wlog, PasslibHashWarning)
# check no default rounds
d1.default_rounds = None
self.assertRaises(TypeError, norm_rounds, use_defaults=True)
def test_40_backends(self):
"test GenericHandler + HasManyBackends mixin"
class d1(uh.HasManyBackends, uh.GenericHandler):
name = 'd1'
setting_kwds = ()
backends = ("a", "b")
_has_backend_a = False
_has_backend_b = False
def _calc_checksum_a(self, secret):
return 'a'
def _calc_checksum_b(self, secret):
return 'b'
# test no backends
self.assertRaises(MissingBackendError, d1.get_backend)
self.assertRaises(MissingBackendError, d1.set_backend)
self.assertRaises(MissingBackendError, d1.set_backend, 'any')
self.assertRaises(MissingBackendError, d1.set_backend, 'default')
self.assertFalse(d1.has_backend())
# enable 'b' backend
d1._has_backend_b = True
# test lazy load
obj = d1()
self.assertEqual(obj._calc_checksum('s'), 'b')
# test repeat load
d1.set_backend('b')
d1.set_backend('any')
self.assertEqual(obj._calc_checksum('s'), 'b')
# test unavailable
self.assertRaises(MissingBackendError, d1.set_backend, 'a')
self.assertTrue(d1.has_backend('b'))
self.assertFalse(d1.has_backend('a'))
# enable 'a' backend also
d1._has_backend_a = True
# test explicit
self.assertTrue(d1.has_backend())
d1.set_backend('a')
self.assertEqual(obj._calc_checksum('s'), 'a')
# test unknown backend
self.assertRaises(ValueError, d1.set_backend, 'c')
self.assertRaises(ValueError, d1.has_backend, 'c')
def test_50_norm_ident(self):
"test GenericHandler + HasManyIdents"
# setup helpers
class d1(uh.HasManyIdents, uh.GenericHandler):
name = 'd1'
setting_kwds = ('ident',)
default_ident = u("!A")
ident_values = [ u("!A"), u("!B") ]
ident_aliases = { u("A"): u("!A")}
def norm_ident(**k):
return d1(**k).ident
# check ident=None
self.assertRaises(TypeError, norm_ident)
self.assertRaises(TypeError, norm_ident, ident=None)
self.assertEqual(norm_ident(use_defaults=True), u('!A'))
# check valid idents
self.assertEqual(norm_ident(ident=u('!A')), u('!A'))
self.assertEqual(norm_ident(ident=u('!B')), u('!B'))
self.assertRaises(ValueError, norm_ident, ident=u('!C'))
# check aliases
self.assertEqual(norm_ident(ident=u('A')), u('!A'))
# check invalid idents
self.assertRaises(ValueError, norm_ident, ident=u('B'))
# check identify is honoring ident system
self.assertTrue(d1.identify(u("!Axxx")))
self.assertTrue(d1.identify(u("!Bxxx")))
self.assertFalse(d1.identify(u("!Cxxx")))
self.assertFalse(d1.identify(u("A")))
self.assertFalse(d1.identify(u("")))
self.assertRaises(TypeError, d1.identify, None)
self.assertRaises(TypeError, d1.identify, 1)
# check default_ident missing is detected.
d1.default_ident = None
self.assertRaises(AssertionError, norm_ident, use_defaults=True)
#===================================================================
# experimental - the following methods are not finished or tested,
# but way work correctly for some hashes
#===================================================================
def test_91_parsehash(self):
"test parsehash()"
# NOTE: this just tests some existing GenericHandler classes
from passlib import hash
#
# parsehash()
#
# simple hash w/ salt
result = hash.des_crypt.parsehash("OgAwTx2l6NADI")
self.assertEqual(result, {'checksum': u('AwTx2l6NADI'), 'salt': u('Og')})
# parse rounds and extra implicit_rounds flag
h = '$5$LKO/Ute40T3FNF95$U0prpBQd4PloSGU0pnpM4z9wKn4vZ1.jsrzQfPqxph9'
s = u('LKO/Ute40T3FNF95')
c = u('U0prpBQd4PloSGU0pnpM4z9wKn4vZ1.jsrzQfPqxph9')
result = hash.sha256_crypt.parsehash(h)
self.assertEqual(result, dict(salt=s, rounds=5000,
implicit_rounds=True, checksum=c))
# omit checksum
result = hash.sha256_crypt.parsehash(h, checksum=False)
self.assertEqual(result, dict(salt=s, rounds=5000, implicit_rounds=True))
# sanitize
result = hash.sha256_crypt.parsehash(h, sanitize=True)
self.assertEqual(result, dict(rounds=5000, implicit_rounds=True,
salt=u('LK**************'),
checksum=u('U0pr***************************************')))
# parse w/o implicit rounds flag
result = hash.sha256_crypt.parsehash('$5$rounds=10428$uy/jIAhCetNCTtb0$YWvUOXbkqlqhyoPMpN8BMe.ZGsGx2aBvxTvDFI613c3')
self.assertEqual(result, dict(
checksum=u('YWvUOXbkqlqhyoPMpN8BMe.ZGsGx2aBvxTvDFI613c3'),
salt=u('uy/jIAhCetNCTtb0'),
rounds=10428,
))
# parsing of raw checksums & salts
h1 = '$pbkdf2$60000$DoEwpvQeA8B4T.k951yLUQ$O26Y3/NJEiLCVaOVPxGXshyjW8k'
result = hash.pbkdf2_sha1.parsehash(h1)
self.assertEqual(result, dict(
checksum=b(';n\x98\xdf\xf3I\x12"\xc2U\xa3\x95?\x11\x97\xb2\x1c\xa3[\xc9'),
rounds=60000,
salt=b('\x0e\x810\xa6\xf4\x1e\x03\xc0xO\xe9=\xe7\\\x8bQ'),
))
# sanitizing of raw checksums & salts
result = hash.pbkdf2_sha1.parsehash(h1, sanitize=True)
self.assertEqual(result, dict(
checksum=u('O26************************'),
rounds=60000,
salt=u('Do********************'),
))
def test_92_bitsize(self):
"test bitsize()"
# NOTE: this just tests some existing GenericHandler classes
from passlib import hash
# no rounds
self.assertEqual(hash.des_crypt.bitsize(),
{'checksum': 66, 'salt': 12})
# log2 rounds
self.assertEqual(hash.bcrypt.bitsize(),
{'checksum': 186, 'salt': 132})
# linear rounds
self.assertEqual(hash.sha256_crypt.bitsize(),
{'checksum': 258, 'rounds': 14, 'salt': 96})
# raw checksum
self.assertEqual(hash.pbkdf2_sha1.bitsize(),
{'checksum': 160, 'rounds': 13, 'salt': 128})
# TODO: handle fshp correctly, and other glitches noted in code.
##self.assertEqual(hash.fshp.bitsize(variant=1),
## {'checksum': 256, 'rounds': 13, 'salt': 128})
#===================================================================
# eoc
#===================================================================
#=============================================================================
# PrefixWrapper
#=============================================================================
class dummy_handler_in_registry(object):
"context manager that inserts dummy handler in registry"
def __init__(self, name):
self.name = name
self.dummy = type('dummy_' + name, (uh.GenericHandler,), dict(
name=name,
setting_kwds=(),
))
def __enter__(self):
from passlib import registry
registry._unload_handler_name(self.name, locations=False)
registry.register_crypt_handler(self.dummy)
assert registry.get_crypt_handler(self.name) is self.dummy
return self.dummy
def __exit__(self, *exc_info):
from passlib import registry
registry._unload_handler_name(self.name, locations=False)
class PrefixWrapperTest(TestCase):
"test PrefixWrapper class"
def test_00_lazy_loading(self):
"test PrefixWrapper lazy loading of handler"
d1 = uh.PrefixWrapper("d1", "ldap_md5", "{XXX}", "{MD5}", lazy=True)
# check base state
self.assertEqual(d1._wrapped_name, "ldap_md5")
self.assertIs(d1._wrapped_handler, None)
# check loading works
self.assertIs(d1.wrapped, ldap_md5)
self.assertIs(d1._wrapped_handler, ldap_md5)
# replace w/ wrong handler, make sure doesn't reload w/ dummy
with dummy_handler_in_registry("ldap_md5") as dummy:
self.assertIs(d1.wrapped, ldap_md5)
def test_01_active_loading(self):
"test PrefixWrapper active loading of handler"
d1 = uh.PrefixWrapper("d1", "ldap_md5", "{XXX}", "{MD5}")
# check base state
self.assertEqual(d1._wrapped_name, "ldap_md5")
self.assertIs(d1._wrapped_handler, ldap_md5)
self.assertIs(d1.wrapped, ldap_md5)
# replace w/ wrong handler, make sure doesn't reload w/ dummy
with dummy_handler_in_registry("ldap_md5") as dummy:
self.assertIs(d1.wrapped, ldap_md5)
def test_02_explicit(self):
"test PrefixWrapper with explicitly specified handler"
d1 = uh.PrefixWrapper("d1", ldap_md5, "{XXX}", "{MD5}")
# check base state
self.assertEqual(d1._wrapped_name, None)
self.assertIs(d1._wrapped_handler, ldap_md5)
self.assertIs(d1.wrapped, ldap_md5)
# replace w/ wrong handler, make sure doesn't reload w/ dummy
with dummy_handler_in_registry("ldap_md5") as dummy:
self.assertIs(d1.wrapped, ldap_md5)
def test_10_wrapped_attributes(self):
d1 = uh.PrefixWrapper("d1", "ldap_md5", "{XXX}", "{MD5}")
self.assertEqual(d1.name, "d1")
self.assertIs(d1.setting_kwds, ldap_md5.setting_kwds)
self.assertFalse('max_rounds' in dir(d1))
d2 = uh.PrefixWrapper("d2", "sha256_crypt", "{XXX}")
self.assertIs(d2.setting_kwds, sha256_crypt.setting_kwds)
if SUPPORTS_DIR_METHOD:
self.assertTrue('max_rounds' in dir(d2))
else:
self.assertFalse('max_rounds' in dir(d2))
def test_11_wrapped_methods(self):
d1 = uh.PrefixWrapper("d1", "ldap_md5", "{XXX}", "{MD5}")
dph = "{XXX}X03MO1qnZdYdgyfeuILPmQ=="
lph = "{MD5}X03MO1qnZdYdgyfeuILPmQ=="
# genconfig
self.assertIs(d1.genconfig(), None)
# genhash
self.assertEqual(d1.genhash("password", None), dph)
self.assertEqual(d1.genhash("password", dph), dph)
self.assertRaises(ValueError, d1.genhash, "password", lph)
# encrypt
self.assertEqual(d1.encrypt("password"), dph)
# identify
self.assertTrue(d1.identify(dph))
self.assertFalse(d1.identify(lph))
# verify
self.assertRaises(ValueError, d1.verify, "password", lph)
self.assertTrue(d1.verify("password", dph))
def test_12_ident(self):
# test ident is proxied
h = uh.PrefixWrapper("h2", "ldap_md5", "{XXX}")
self.assertEqual(h.ident, u("{XXX}{MD5}"))
self.assertIs(h.ident_values, None)
# test lack of ident means no proxy
h = uh.PrefixWrapper("h2", "des_crypt", "{XXX}")
self.assertIs(h.ident, None)
self.assertIs(h.ident_values, None)
# test orig_prefix disabled ident proxy
h = uh.PrefixWrapper("h1", "ldap_md5", "{XXX}", "{MD5}")
self.assertIs(h.ident, None)
self.assertIs(h.ident_values, None)
# test custom ident overrides default
h = uh.PrefixWrapper("h3", "ldap_md5", "{XXX}", ident="{X")
self.assertEqual(h.ident, u("{X"))
self.assertIs(h.ident_values, None)
# test custom ident must match
h = uh.PrefixWrapper("h3", "ldap_md5", "{XXX}", ident="{XXX}A")
self.assertRaises(ValueError, uh.PrefixWrapper, "h3", "ldap_md5",
"{XXX}", ident="{XY")
self.assertRaises(ValueError, uh.PrefixWrapper, "h3", "ldap_md5",
"{XXX}", ident="{XXXX")
# test ident_values is proxied
h = uh.PrefixWrapper("h4", "phpass", "{XXX}")
self.assertIs(h.ident, None)
self.assertEqual(h.ident_values, [ u("{XXX}$P$"), u("{XXX}$H$") ])
# test ident=True means use prefix even if hash has no ident.
h = uh.PrefixWrapper("h5", "des_crypt", "{XXX}", ident=True)
self.assertEqual(h.ident, u("{XXX}"))
self.assertIs(h.ident_values, None)
# ... but requires prefix
self.assertRaises(ValueError, uh.PrefixWrapper, "h6", "des_crypt", ident=True)
# orig_prefix + HasManyIdent - warning
with self.assertWarningList("orig_prefix.*may not work correctly"):
h = uh.PrefixWrapper("h7", "phpass", orig_prefix="$", prefix="?")
self.assertEqual(h.ident_values, None) # TODO: should output (u("?P$"), u("?H$")))
self.assertEqual(h.ident, None)
def test_13_repr(self):
"test repr()"
h = uh.PrefixWrapper("h2", "md5_crypt", "{XXX}", orig_prefix="$1$")
self.assertRegex(repr(h),
r"""(?x)^PrefixWrapper\(
['"]h2['"],\s+
['"]md5_crypt['"],\s+
prefix=u?["']{XXX}['"],\s+
orig_prefix=u?["']\$1\$['"]
\)$""")
def test_14_bad_hash(self):
"test orig_prefix sanity check"
# shoudl throw InvalidHashError if wrapped hash doesn't begin
# with orig_prefix.
h = uh.PrefixWrapper("h2", "md5_crypt", orig_prefix="$6$")
self.assertRaises(ValueError, h.encrypt, 'test')
#=============================================================================
# sample algorithms - these serve as known quantities
# to test the unittests themselves, as well as other
# parts of passlib. they shouldn't be used as actual password schemes.
#=============================================================================
class UnsaltedHash(uh.StaticHandler):
"test algorithm which lacks a salt"
name = "unsalted_test_hash"
checksum_chars = uh.LOWER_HEX_CHARS
checksum_size = 40
def _calc_checksum(self, secret):
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
data = b("boblious") + secret
return str_to_uascii(hashlib.sha1(data).hexdigest())
class SaltedHash(uh.HasSalt, uh.GenericHandler):
"test algorithm with a salt"
name = "salted_test_hash"
setting_kwds = ("salt",)
min_salt_size = 2
max_salt_size = 4
checksum_size = 40
salt_chars = checksum_chars = uh.LOWER_HEX_CHARS
_hash_regex = re.compile(u("^@salt[0-9a-f]{42,44}$"))
@classmethod
def from_string(cls, hash):
if not cls.identify(hash):
raise uh.exc.InvalidHashError(cls)
if isinstance(hash, bytes):
hash = hash.decode("ascii")
return cls(salt=hash[5:-40], checksum=hash[-40:])
_stub_checksum = u('0') * 40
def to_string(self):
hash = u("@salt%s%s") % (self.salt, self.checksum or self._stub_checksum)
return uascii_to_str(hash)
def _calc_checksum(self, secret):
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
data = self.salt.encode("ascii") + secret + self.salt.encode("ascii")
return str_to_uascii(hashlib.sha1(data).hexdigest())
#=============================================================================
# test sample algorithms - really a self-test of HandlerCase
#=============================================================================
# TODO: provide data samples for algorithms
# (positive knowns, negative knowns, invalid identify)
UPASS_TEMP = u('\u0399\u03c9\u03b1\u03bd\u03bd\u03b7\u03c2')
class UnsaltedHashTest(HandlerCase):
handler = UnsaltedHash
known_correct_hashes = [
("password", "61cfd32684c47de231f1f982c214e884133762c0"),
(UPASS_TEMP, '96b329d120b97ff81ada770042e44ba87343ad2b'),
]
def test_bad_kwds(self):
if not PY_MAX_25:
# annoyingly, py25's ``super().__init__()`` doesn't throw TypeError
# when passing unknown keywords to object. just ignoring
# this issue for now, since it's a minor border case.
self.assertRaises(TypeError, UnsaltedHash, salt='x')
self.assertRaises(TypeError, UnsaltedHash.genconfig, rounds=1)
class SaltedHashTest(HandlerCase):
handler = SaltedHash
known_correct_hashes = [
("password", '@salt77d71f8fe74f314dac946766c1ac4a2a58365482c0'),
(UPASS_TEMP, '@salt9f978a9bfe360d069b0c13f2afecd570447407fa7e48'),
]
def test_bad_kwds(self):
self.assertRaises(TypeError, SaltedHash,
checksum=SaltedHash._stub_checksum, salt=None)
self.assertRaises(ValueError, SaltedHash,
checksum=SaltedHash._stub_checksum, salt='xxx')
#=============================================================================
# eof
#=============================================================================

View File

@ -0,0 +1,51 @@
"""tests for passlib.win32 -- (c) Assurance Technologies 2003-2009"""
#=============================================================================
# imports
#=============================================================================
# core
from binascii import hexlify
import warnings
# site
# pkg
from passlib.tests.utils import TestCase
# module
from passlib.utils.compat import u
#=============================================================================
#
#=============================================================================
class UtilTest(TestCase):
"test util funcs in passlib.win32"
##test hashes from http://msdn.microsoft.com/en-us/library/cc245828(v=prot.10).aspx
## among other places
def setUp(self):
super(UtilTest, self).setUp()
warnings.filterwarnings("ignore",
"the 'passlib.win32' module is deprecated")
def test_lmhash(self):
from passlib.win32 import raw_lmhash
for secret, hash in [
("OLDPASSWORD", u("c9b81d939d6fd80cd408e6b105741864")),
("NEWPASSWORD", u('09eeab5aa415d6e4d408e6b105741864')),
("welcome", u("c23413a8a1e7665faad3b435b51404ee")),
]:
result = raw_lmhash(secret, hex=True)
self.assertEqual(result, hash)
def test_nthash(self):
warnings.filterwarnings("ignore",
r"nthash\.raw_nthash\(\) is deprecated")
from passlib.win32 import raw_nthash
for secret, hash in [
("OLDPASSWORD", u("6677b2c394311355b54f25eec5bfacf5")),
("NEWPASSWORD", u("256781a62031289d3c2c98c14f1efc8c")),
]:
result = raw_nthash(secret, hex=True)
self.assertEqual(result, hash)
#=============================================================================
# eof
#=============================================================================

View File

@ -0,0 +1,83 @@
"""passlib.tests.tox_support - helper script for tox tests"""
#=============================================================================
# init script env
#=============================================================================
import os, sys
root_dir = os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)
sys.path.insert(0, root_dir)
#=============================================================================
# imports
#=============================================================================
# core
import re
import logging; log = logging.getLogger(__name__)
# site
# pkg
from passlib.utils.compat import print_
# local
__all__ = [
]
#=============================================================================
# main
#=============================================================================
TH_PATH = "passlib.tests.test_handlers"
def do_hash_tests(*args):
"return list of hash algorithm tests that match regexes"
if not args:
print(TH_PATH)
return
suffix = ''
args = list(args)
while True:
if args[0] == "--method":
suffix = '.' + args[1]
del args[:2]
else:
break
from passlib.tests import test_handlers
names = [TH_PATH + ":" + name + suffix for name in dir(test_handlers)
if not name.startswith("_") and any(re.match(arg,name) for arg in args)]
print_("\n".join(names))
return not names
def do_preset_tests(name):
"return list of preset test names"
if name == "django" or name == "django-hashes":
do_hash_tests("django_.*_test", "hex_md5_test")
if name == "django":
print_("passlib.tests.test_ext_django")
else:
raise ValueError("unknown name: %r" % name)
def do_setup_gae(path, runtime):
"write fake GAE ``app.yaml`` to current directory so nosegae will work"
from passlib.tests.utils import set_file
set_file(os.path.join(path, "app.yaml"), """\
application: fake-app
version: 2
runtime: %s
api_version: 1
threadsafe: no
handlers:
- url: /.*
script: dummy.py
libraries:
- name: django
version: "latest"
""" % runtime)
def main(cmd, *args):
return globals()["do_" + cmd](*args)
if __name__ == "__main__":
import sys
sys.exit(main(*sys.argv[1:]) or 0)
#=============================================================================
# eof
#=============================================================================

2252
passlib/tests/utils.py Normal file

File diff suppressed because it is too large Load Diff

1619
passlib/utils/__init__.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,172 @@
"""passlib.utils._blowfish - pure-python eks-blowfish implementation for bcrypt
This is a pure-python implementation of the EKS-Blowfish algorithm described by
Provos and Mazieres in `A Future-Adaptable Password Scheme
<http://www.openbsd.org/papers/bcrypt-paper.ps>`_.
This package contains two submodules:
* ``_blowfish/base.py`` contains a class implementing the eks-blowfish algorithm
using easy-to-examine code.
* ``_blowfish/unrolled.py`` contains a subclass which replaces some methods
of the original class with sped-up versions, mainly using unrolled loops
and local variables. this is the class which is actually used by
Passlib to perform BCrypt in pure python.
This module is auto-generated by a script, ``_blowfish/_gen_files.py``.
Status
------
This implementation is usuable, but is an order of magnitude too slow to be
usuable with real security. For "ok" security, BCrypt hashes should have at
least 2**11 rounds (as of 2011). Assuming a desired response time <= 100ms,
this means a BCrypt implementation should get at least 20 rounds/ms in order
to be both usuable *and* secure. On a 2 ghz cpu, this implementation gets
roughly 0.09 rounds/ms under CPython (220x too slow), and 1.9 rounds/ms
under PyPy (10x too slow).
History
-------
While subsequently modified considerly for Passlib, this code was originally
based on `jBcrypt 0.2 <http://www.mindrot.org/projects/jBCrypt/>`_, which was
released under the BSD license::
Copyright (c) 2006 Damien Miller <djm@mindrot.org>
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
"""
#=============================================================================
# imports
#=============================================================================
# core
from itertools import chain
import struct
# pkg
from passlib.utils import bcrypt64, getrandbytes, rng
from passlib.utils.compat import b, bytes, BytesIO, unicode, u
from passlib.utils._blowfish.unrolled import BlowfishEngine
# local
__all__ = [
'BlowfishEngine',
'raw_bcrypt',
]
#=============================================================================
# bcrypt constants
#=============================================================================
# bcrypt constant data "OrpheanBeholderScryDoubt" as 6 integers
BCRYPT_CDATA = [
0x4f727068, 0x65616e42, 0x65686f6c,
0x64657253, 0x63727944, 0x6f756274
]
# struct used to encode ciphertext as digest (last output byte discarded)
digest_struct = struct.Struct(">6I")
#=============================================================================
# base bcrypt helper
#
# interface designed only for use by passlib.handlers.bcrypt:BCrypt
# probably not suitable for other purposes
#=============================================================================
BNULL = b('\x00')
def raw_bcrypt(password, ident, salt, log_rounds):
"""perform central password hashing step in bcrypt scheme.
:param password: the password to hash
:param ident: identifier w/ minor version (e.g. 2, 2a)
:param salt: the binary salt to use (encoded in bcrypt-base64)
:param rounds: the log2 of the number of rounds (as int)
:returns: bcrypt-base64 encoded checksum
"""
#===================================================================
# parse inputs
#===================================================================
# parse ident
assert isinstance(ident, unicode)
if ident == u('2'):
minor = 0
elif ident == u('2a'):
minor = 1
# XXX: how to indicate caller wants to use crypt_blowfish's
# workaround variant of 2a?
elif ident == u('2x'):
raise ValueError("crypt_blowfish's buggy '2x' hashes are not "
"currently supported")
elif ident == u('2y'):
# crypt_blowfish compatibility ident which guarantees compat w/ 2a
minor = 1
else:
raise ValueError("unknown ident: %r" % (ident,))
# decode & validate salt
assert isinstance(salt, bytes)
salt = bcrypt64.decode_bytes(salt)
if len(salt) < 16:
raise ValueError("Missing salt bytes")
elif len(salt) > 16:
salt = salt[:16]
# prepare password
assert isinstance(password, bytes)
if minor > 0:
password += BNULL
# validate rounds
if log_rounds < 4 or log_rounds > 31:
raise ValueError("Bad number of rounds")
#===================================================================
#
# run EKS-Blowfish algorithm
#
# This uses the "enhanced key schedule" step described by
# Provos and Mazieres in "A Future-Adaptable Password Scheme"
# http://www.openbsd.org/papers/bcrypt-paper.ps
#
#===================================================================
engine = BlowfishEngine()
# convert password & salt into list of 18 32-bit integers (72 bytes total).
pass_words = engine.key_to_words(password)
salt_words = engine.key_to_words(salt)
# truncate salt_words to original 16 byte salt, or loop won't wrap
# correctly when passed to .eks_salted_expand()
salt_words16 = salt_words[:4]
# do EKS key schedule setup
engine.eks_salted_expand(pass_words, salt_words16)
# apply password & salt keys to key schedule a bunch more times.
rounds = 1<<log_rounds
engine.eks_repeated_expand(pass_words, salt_words, rounds)
# encipher constant data, and encode to bytes as digest.
data = list(BCRYPT_CDATA)
i = 0
while i < 6:
data[i], data[i+1] = engine.repeat_encipher(data[i], data[i+1], 64)
i += 2
raw = digest_struct.pack(*data)[:-1]
return bcrypt64.encode_bytes(raw)
#=============================================================================
# eof
#=============================================================================

View File

@ -0,0 +1,204 @@
"""passlib.utils._blowfish._gen_files - meta script that generates unrolled.py"""
#=============================================================================
# imports
#=============================================================================
# core
import os
import textwrap
# pkg
from passlib.utils.compat import irange
# local
#=============================================================================
# helpers
#=============================================================================
def varlist(name, count):
return ", ".join(name + str(x) for x in irange(count))
def indent_block(block, padding):
"ident block of text"
lines = block.split("\n")
return "\n".join(
padding + line if line else ""
for line in lines
)
BFSTR = """\
((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff)
""".strip()
def render_encipher(write, indent=0):
for i in irange(0, 15, 2):
write(indent, """\
# Feistel substitution on left word (round %(i)d)
r ^= %(left)s ^ p%(i1)d
# Feistel substitution on right word (round %(i1)d)
l ^= %(right)s ^ p%(i2)d
""", i=i, i1=i+1, i2=i+2,
left=BFSTR, right=BFSTR.replace("l","r"),
)
def write_encipher_function(write, indent=0):
write(indent, """\
def encipher(self, l, r):
\"""blowfish encipher a single 64-bit block encoded as two 32-bit ints\"""
(p0, p1, p2, p3, p4, p5, p6, p7, p8, p9,
p10, p11, p12, p13, p14, p15, p16, p17) = self.P
S0, S1, S2, S3 = self.S
l ^= p0
""")
render_encipher(write, indent+1)
write(indent+1, """\
return r ^ p17, l
""")
def write_expand_function(write, indent=0):
write(indent, """\
def expand(self, key_words):
\"""unrolled version of blowfish key expansion\"""
##assert len(key_words) >= 18, "size of key_words must be >= 18"
P, S = self.P, self.S
S0, S1, S2, S3 = S
#=============================================================
# integrate key
#=============================================================
""")
for i in irange(18):
write(indent+1, """\
p%(i)d = P[%(i)d] ^ key_words[%(i)d]
""", i=i)
write(indent+1, """\
#=============================================================
# update P
#=============================================================
#------------------------------------------------
# update P[0] and P[1]
#------------------------------------------------
l, r = p0, 0
""")
render_encipher(write, indent+1)
write(indent+1, """\
p0, p1 = l, r = r ^ p17, l
""")
for i in irange(2, 18, 2):
write(indent+1, """\
#------------------------------------------------
# update P[%(i)d] and P[%(i1)d]
#------------------------------------------------
l ^= p0
""", i=i, i1=i+1)
render_encipher(write, indent+1)
write(indent+1, """\
p%(i)d, p%(i1)d = l, r = r ^ p17, l
""", i=i, i1=i+1)
write(indent+1, """\
#------------------------------------------------
# save changes to original P array
#------------------------------------------------
P[:] = (p0, p1, p2, p3, p4, p5, p6, p7, p8, p9,
p10, p11, p12, p13, p14, p15, p16, p17)
#=============================================================
# update S
#=============================================================
for box in S:
j = 0
while j < 256:
l ^= p0
""")
render_encipher(write, indent+3)
write(indent+3, """\
box[j], box[j+1] = l, r = r ^ p17, l
j += 2
""")
#=============================================================================
# main
#=============================================================================
def main():
target = os.path.join(os.path.dirname(__file__), "unrolled.py")
fh = file(target, "w")
def write(indent, msg, **kwds):
literal = kwds.pop("literal", False)
if kwds:
msg %= kwds
if not literal:
msg = textwrap.dedent(msg.rstrip(" "))
if indent:
msg = indent_block(msg, " " * (indent*4))
fh.write(msg)
write(0, """\
\"""passlib.utils._blowfish.unrolled - unrolled loop implementation of bcrypt,
autogenerated by _gen_files.py
currently this override the encipher() and expand() methods
with optimized versions, and leaves the other base.py methods alone.
\"""
#=================================================================
# imports
#=================================================================
# pkg
from passlib.utils._blowfish.base import BlowfishEngine as _BlowfishEngine
# local
__all__ = [
"BlowfishEngine",
]
#=================================================================
#
#=================================================================
class BlowfishEngine(_BlowfishEngine):
""")
write_encipher_function(write, indent=1)
write_expand_function(write, indent=1)
write(0, """\
#=================================================================
# eoc
#=================================================================
#=================================================================
# eof
#=================================================================
""")
if __name__ == "__main__":
main()
#=============================================================================
# eof
#=============================================================================

View File

@ -0,0 +1,442 @@
"""passlib.utils._blowfish.base - unoptimized pure-python blowfish engine"""
#=============================================================================
# imports
#=============================================================================
# core
import struct
# pkg
from passlib.utils.compat import bytes
from passlib.utils import repeat_string
# local
__all__ = [
"BlowfishEngine",
]
#=============================================================================
# blowfish constants
#=============================================================================
BLOWFISH_P = BLOWFISH_S = None
def _init_constants():
global BLOWFISH_P, BLOWFISH_S
# NOTE: blowfish's spec states these numbers are the hex representation
# of the fractional portion of PI, in order.
# Initial contents of key schedule - 18 integers
BLOWFISH_P = [
0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344,
0xa4093822, 0x299f31d0, 0x082efa98, 0xec4e6c89,
0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c,
0xc0ac29b7, 0xc97c50dd, 0x3f84d5b5, 0xb5470917,
0x9216d5d9, 0x8979fb1b,
]
# all 4 blowfish S boxes in one array - 256 integers per S box
BLOWFISH_S = [
# sbox 1
[
0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7,
0xb8e1afed, 0x6a267e96, 0xba7c9045, 0xf12c7f99,
0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16,
0x636920d8, 0x71574e69, 0xa458fea3, 0xf4933d7e,
0x0d95748f, 0x728eb658, 0x718bcd58, 0x82154aee,
0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013,
0xc5d1b023, 0x286085f0, 0xca417918, 0xb8db38ef,
0x8e79dcb0, 0x603a180e, 0x6c9e0e8b, 0xb01e8a3e,
0xd71577c1, 0xbd314b27, 0x78af2fda, 0x55605c60,
0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440,
0x55ca396a, 0x2aab10b6, 0xb4cc5c34, 0x1141e8ce,
0xa15486af, 0x7c72e993, 0xb3ee1411, 0x636fbc2a,
0x2ba9c55d, 0x741831f6, 0xce5c3e16, 0x9b87931e,
0xafd6ba33, 0x6c24cf5c, 0x7a325381, 0x28958677,
0x3b8f4898, 0x6b4bb9af, 0xc4bfe81b, 0x66282193,
0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032,
0xef845d5d, 0xe98575b1, 0xdc262302, 0xeb651b88,
0x23893e81, 0xd396acc5, 0x0f6d6ff3, 0x83f44239,
0x2e0b4482, 0xa4842004, 0x69c8f04a, 0x9e1f9b5e,
0x21c66842, 0xf6e96c9a, 0x670c9c61, 0xabd388f0,
0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3,
0x6eef0b6c, 0x137a3be4, 0xba3bf050, 0x7efb2a98,
0xa1f1651d, 0x39af0176, 0x66ca593e, 0x82430e88,
0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 0x3b8b5ebe,
0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6,
0x4ed3aa62, 0x363f7706, 0x1bfedf72, 0x429b023d,
0x37d0d724, 0xd00a1248, 0xdb0fead3, 0x49f1c09b,
0x075372c9, 0x80991b7b, 0x25d479d8, 0xf6e8def7,
0xe3fe501a, 0xb6794c3b, 0x976ce0bd, 0x04c006ba,
0xc1a94fb6, 0x409f60c4, 0x5e5c9ec2, 0x196a2463,
0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f,
0x6dfc511f, 0x9b30952c, 0xcc814544, 0xaf5ebd09,
0xbee3d004, 0xde334afd, 0x660f2807, 0x192e4bb3,
0xc0cba857, 0x45c8740f, 0xd20b5f39, 0xb9d3fbdb,
0x5579c0bd, 0x1a60320a, 0xd6a100c6, 0x402c7279,
0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8,
0x3c7516df, 0xfd616b15, 0x2f501ec8, 0xad0552ab,
0x323db5fa, 0xfd238760, 0x53317b48, 0x3e00df82,
0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 0xdf1769db,
0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573,
0x695b27b0, 0xbbca58c8, 0xe1ffa35d, 0xb8f011a0,
0x10fa3d98, 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b,
0x9a53e479, 0xb6f84565, 0xd28e49bc, 0x4bfb9790,
0xe1ddf2da, 0xa4cb7e33, 0x62fb1341, 0xcee4c6e8,
0xef20cada, 0x36774c01, 0xd07e9efe, 0x2bf11fb4,
0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0,
0xd08ed1d0, 0xafc725e0, 0x8e3c5b2f, 0x8e7594b7,
0x8ff6e2fb, 0xf2122b64, 0x8888b812, 0x900df01c,
0x4fad5ea0, 0x688fc31c, 0xd1cff191, 0xb3a8c1ad,
0x2f2f2218, 0xbe0e1777, 0xea752dfe, 0x8b021fa1,
0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299,
0xb4a84fe0, 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9,
0x165fa266, 0x80957705, 0x93cc7314, 0x211a1477,
0xe6ad2065, 0x77b5fa86, 0xc75442f5, 0xfb9d35cf,
0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49,
0x00250e2d, 0x2071b35e, 0x226800bb, 0x57b8e0af,
0x2464369b, 0xf009b91e, 0x5563911d, 0x59dfa6aa,
0x78c14389, 0xd95a537f, 0x207d5ba2, 0x02e5b9c5,
0x83260376, 0x6295cfa9, 0x11c81968, 0x4e734a41,
0xb3472dca, 0x7b14a94a, 0x1b510052, 0x9a532915,
0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400,
0x08ba6fb5, 0x571be91f, 0xf296ec6b, 0x2a0dd915,
0xb6636521, 0xe7b9f9b6, 0xff34052e, 0xc5855664,
0x53b02d5d, 0xa99f8fa1, 0x08ba4799, 0x6e85076a,
],
# sbox 2
[
0x4b7a70e9, 0xb5b32944, 0xdb75092e, 0xc4192623,
0xad6ea6b0, 0x49a7df7d, 0x9cee60b8, 0x8fedb266,
0xecaa8c71, 0x699a17ff, 0x5664526c, 0xc2b19ee1,
0x193602a5, 0x75094c29, 0xa0591340, 0xe4183a3e,
0x3f54989a, 0x5b429d65, 0x6b8fe4d6, 0x99f73fd6,
0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1,
0x4cdd2086, 0x8470eb26, 0x6382e9c6, 0x021ecc5e,
0x09686b3f, 0x3ebaefc9, 0x3c971814, 0x6b6a70a1,
0x687f3584, 0x52a0e286, 0xb79c5305, 0xaa500737,
0x3e07841c, 0x7fdeae5c, 0x8e7d44ec, 0x5716f2b8,
0xb03ada37, 0xf0500c0d, 0xf01c1f04, 0x0200b3ff,
0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd,
0xd19113f9, 0x7ca92ff6, 0x94324773, 0x22f54701,
0x3ae5e581, 0x37c2dadc, 0xc8b57634, 0x9af3dda7,
0xa9446146, 0x0fd0030e, 0xecc8c73e, 0xa4751e41,
0xe238cd99, 0x3bea0e2f, 0x3280bba1, 0x183eb331,
0x4e548b38, 0x4f6db908, 0x6f420d03, 0xf60a04bf,
0x2cb81290, 0x24977c79, 0x5679b072, 0xbcaf89af,
0xde9a771f, 0xd9930810, 0xb38bae12, 0xdccf3f2e,
0x5512721f, 0x2e6b7124, 0x501adde6, 0x9f84cd87,
0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c,
0xec7aec3a, 0xdb851dfa, 0x63094366, 0xc464c3d2,
0xef1c1847, 0x3215d908, 0xdd433b37, 0x24c2ba16,
0x12a14d43, 0x2a65c451, 0x50940002, 0x133ae4dd,
0x71dff89e, 0x10314e55, 0x81ac77d6, 0x5f11199b,
0x043556f1, 0xd7a3c76b, 0x3c11183b, 0x5924a509,
0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e,
0x86e34570, 0xeae96fb1, 0x860e5e0a, 0x5a3e2ab3,
0x771fe71c, 0x4e3d06fa, 0x2965dcb9, 0x99e71d0f,
0x803e89d6, 0x5266c825, 0x2e4cc978, 0x9c10b36a,
0xc6150eba, 0x94e2ea78, 0xa5fc3c53, 0x1e0a2df4,
0xf2f74ea7, 0x361d2b3d, 0x1939260f, 0x19c27960,
0x5223a708, 0xf71312b6, 0xebadfe6e, 0xeac31f66,
0xe3bc4595, 0xa67bc883, 0xb17f37d1, 0x018cff28,
0xc332ddef, 0xbe6c5aa5, 0x65582185, 0x68ab9802,
0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84,
0x1521b628, 0x29076170, 0xecdd4775, 0x619f1510,
0x13cca830, 0xeb61bd96, 0x0334fe1e, 0xaa0363cf,
0xb5735c90, 0x4c70a239, 0xd59e9e0b, 0xcbaade14,
0xeecc86bc, 0x60622ca7, 0x9cab5cab, 0xb2f3846e,
0x648b1eaf, 0x19bdf0ca, 0xa02369b9, 0x655abb50,
0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7,
0x9b540b19, 0x875fa099, 0x95f7997e, 0x623d7da8,
0xf837889a, 0x97e32d77, 0x11ed935f, 0x16681281,
0x0e358829, 0xc7e61fd6, 0x96dedfa1, 0x7858ba99,
0x57f584a5, 0x1b227263, 0x9b83c3ff, 0x1ac24696,
0xcdb30aeb, 0x532e3054, 0x8fd948e4, 0x6dbc3128,
0x58ebf2ef, 0x34c6ffea, 0xfe28ed61, 0xee7c3c73,
0x5d4a14d9, 0xe864b7e3, 0x42105d14, 0x203e13e0,
0x45eee2b6, 0xa3aaabea, 0xdb6c4f15, 0xfacb4fd0,
0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105,
0xd81e799e, 0x86854dc7, 0xe44b476a, 0x3d816250,
0xcf62a1f2, 0x5b8d2646, 0xfc8883a0, 0xc1c7b6a3,
0x7f1524c3, 0x69cb7492, 0x47848a0b, 0x5692b285,
0x095bbf00, 0xad19489d, 0x1462b174, 0x23820e00,
0x58428d2a, 0x0c55f5ea, 0x1dadf43e, 0x233f7061,
0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb,
0x7cde3759, 0xcbee7460, 0x4085f2a7, 0xce77326e,
0xa6078084, 0x19f8509e, 0xe8efd855, 0x61d99735,
0xa969a7aa, 0xc50c06c2, 0x5a04abfc, 0x800bcadc,
0x9e447a2e, 0xc3453484, 0xfdd56705, 0x0e1e9ec9,
0xdb73dbd3, 0x105588cd, 0x675fda79, 0xe3674340,
0xc5c43465, 0x713e38d8, 0x3d28f89e, 0xf16dff20,
0x153e21e7, 0x8fb03d4a, 0xe6e39f2b, 0xdb83adf7,
],
# sbox 3
[
0xe93d5a68, 0x948140f7, 0xf64c261c, 0x94692934,
0x411520f7, 0x7602d4f7, 0xbcf46b2e, 0xd4a20068,
0xd4082471, 0x3320f46a, 0x43b7d4b7, 0x500061af,
0x1e39f62e, 0x97244546, 0x14214f74, 0xbf8b8840,
0x4d95fc1d, 0x96b591af, 0x70f4ddd3, 0x66a02f45,
0xbfbc09ec, 0x03bd9785, 0x7fac6dd0, 0x31cb8504,
0x96eb27b3, 0x55fd3941, 0xda2547e6, 0xabca0a9a,
0x28507825, 0x530429f4, 0x0a2c86da, 0xe9b66dfb,
0x68dc1462, 0xd7486900, 0x680ec0a4, 0x27a18dee,
0x4f3ffea2, 0xe887ad8c, 0xb58ce006, 0x7af4d6b6,
0xaace1e7c, 0xd3375fec, 0xce78a399, 0x406b2a42,
0x20fe9e35, 0xd9f385b9, 0xee39d7ab, 0x3b124e8b,
0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2,
0x3a6efa74, 0xdd5b4332, 0x6841e7f7, 0xca7820fb,
0xfb0af54e, 0xd8feb397, 0x454056ac, 0xba489527,
0x55533a3a, 0x20838d87, 0xfe6ba9b7, 0xd096954b,
0x55a867bc, 0xa1159a58, 0xcca92963, 0x99e1db33,
0xa62a4a56, 0x3f3125f9, 0x5ef47e1c, 0x9029317c,
0xfdf8e802, 0x04272f70, 0x80bb155c, 0x05282ce3,
0x95c11548, 0xe4c66d22, 0x48c1133f, 0xc70f86dc,
0x07f9c9ee, 0x41041f0f, 0x404779a4, 0x5d886e17,
0x325f51eb, 0xd59bc0d1, 0xf2bcc18f, 0x41113564,
0x257b7834, 0x602a9c60, 0xdff8e8a3, 0x1f636c1b,
0x0e12b4c2, 0x02e1329e, 0xaf664fd1, 0xcad18115,
0x6b2395e0, 0x333e92e1, 0x3b240b62, 0xeebeb922,
0x85b2a20e, 0xe6ba0d99, 0xde720c8c, 0x2da2f728,
0xd0127845, 0x95b794fd, 0x647d0862, 0xe7ccf5f0,
0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e,
0x0a476341, 0x992eff74, 0x3a6f6eab, 0xf4f8fd37,
0xa812dc60, 0xa1ebddf8, 0x991be14c, 0xdb6e6b0d,
0xc67b5510, 0x6d672c37, 0x2765d43b, 0xdcd0e804,
0xf1290dc7, 0xcc00ffa3, 0xb5390f92, 0x690fed0b,
0x667b9ffb, 0xcedb7d9c, 0xa091cf0b, 0xd9155ea3,
0xbb132f88, 0x515bad24, 0x7b9479bf, 0x763bd6eb,
0x37392eb3, 0xcc115979, 0x8026e297, 0xf42e312d,
0x6842ada7, 0xc66a2b3b, 0x12754ccc, 0x782ef11c,
0x6a124237, 0xb79251e7, 0x06a1bbe6, 0x4bfb6350,
0x1a6b1018, 0x11caedfa, 0x3d25bdd8, 0xe2e1c3c9,
0x44421659, 0x0a121386, 0xd90cec6e, 0xd5abea2a,
0x64af674e, 0xda86a85f, 0xbebfe988, 0x64e4c3fe,
0x9dbc8057, 0xf0f7c086, 0x60787bf8, 0x6003604d,
0xd1fd8346, 0xf6381fb0, 0x7745ae04, 0xd736fccc,
0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f,
0x77a057be, 0xbde8ae24, 0x55464299, 0xbf582e61,
0x4e58f48f, 0xf2ddfda2, 0xf474ef38, 0x8789bdc2,
0x5366f9c3, 0xc8b38e74, 0xb475f255, 0x46fcd9b9,
0x7aeb2661, 0x8b1ddf84, 0x846a0e79, 0x915f95e2,
0x466e598e, 0x20b45770, 0x8cd55591, 0xc902de4c,
0xb90bace1, 0xbb8205d0, 0x11a86248, 0x7574a99e,
0xb77f19b6, 0xe0a9dc09, 0x662d09a1, 0xc4324633,
0xe85a1f02, 0x09f0be8c, 0x4a99a025, 0x1d6efe10,
0x1ab93d1d, 0x0ba5a4df, 0xa186f20f, 0x2868f169,
0xdcb7da83, 0x573906fe, 0xa1e2ce9b, 0x4fcd7f52,
0x50115e01, 0xa70683fa, 0xa002b5c4, 0x0de6d027,
0x9af88c27, 0x773f8641, 0xc3604c06, 0x61a806b5,
0xf0177a28, 0xc0f586e0, 0x006058aa, 0x30dc7d62,
0x11e69ed7, 0x2338ea63, 0x53c2dd94, 0xc2c21634,
0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76,
0x6f05e409, 0x4b7c0188, 0x39720a3d, 0x7c927c24,
0x86e3725f, 0x724d9db9, 0x1ac15bb4, 0xd39eb8fc,
0xed545578, 0x08fca5b5, 0xd83d7cd3, 0x4dad0fc4,
0x1e50ef5e, 0xb161e6f8, 0xa28514d9, 0x6c51133c,
0x6fd5c7e7, 0x56e14ec4, 0x362abfce, 0xddc6c837,
0xd79a3234, 0x92638212, 0x670efa8e, 0x406000e0,
],
# sbox 4
[
0x3a39ce37, 0xd3faf5cf, 0xabc27737, 0x5ac52d1b,
0x5cb0679e, 0x4fa33742, 0xd3822740, 0x99bc9bbe,
0xd5118e9d, 0xbf0f7315, 0xd62d1c7e, 0xc700c47b,
0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4,
0x5748ab2f, 0xbc946e79, 0xc6a376d2, 0x6549c2c8,
0x530ff8ee, 0x468dde7d, 0xd5730a1d, 0x4cd04dc6,
0x2939bbdb, 0xa9ba4650, 0xac9526e8, 0xbe5ee304,
0xa1fad5f0, 0x6a2d519a, 0x63ef8ce2, 0x9a86ee22,
0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4,
0x83c061ba, 0x9be96a4d, 0x8fe51550, 0xba645bd6,
0x2826a2f9, 0xa73a3ae1, 0x4ba99586, 0xef5562e9,
0xc72fefd3, 0xf752f7da, 0x3f046f69, 0x77fa0a59,
0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593,
0xe990fd5a, 0x9e34d797, 0x2cf0b7d9, 0x022b8b51,
0x96d5ac3a, 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28,
0x1f9f25cf, 0xadf2b89b, 0x5ad6b472, 0x5a88f54c,
0xe029ac71, 0xe019a5e6, 0x47b0acfd, 0xed93fa9b,
0xe8d3c48d, 0x283b57cc, 0xf8d56629, 0x79132e28,
0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c,
0x15056dd4, 0x88f46dba, 0x03a16125, 0x0564f0bd,
0xc3eb9e15, 0x3c9057a2, 0x97271aec, 0xa93a072a,
0x1b3f6d9b, 0x1e6321f5, 0xf59c66fb, 0x26dcf319,
0x7533d928, 0xb155fdf5, 0x03563482, 0x8aba3cbb,
0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f,
0x4de81751, 0x3830dc8e, 0x379d5862, 0x9320f991,
0xea7a90c2, 0xfb3e7bce, 0x5121ce64, 0x774fbe32,
0xa8b6e37e, 0xc3293d46, 0x48de5369, 0x6413e680,
0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166,
0xb39a460a, 0x6445c0dd, 0x586cdecf, 0x1c20c8ae,
0x5bbef7dd, 0x1b588d40, 0xccd2017f, 0x6bb4e3bb,
0xdda26a7e, 0x3a59ff45, 0x3e350a44, 0xbcb4cdd5,
0x72eacea8, 0xfa6484bb, 0x8d6612ae, 0xbf3c6f47,
0xd29be463, 0x542f5d9e, 0xaec2771b, 0xf64e6370,
0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d,
0x4040cb08, 0x4eb4e2cc, 0x34d2466a, 0x0115af84,
0xe1b00428, 0x95983a1d, 0x06b89fb4, 0xce6ea048,
0x6f3f3b82, 0x3520ab82, 0x011a1d4b, 0x277227f8,
0x611560b1, 0xe7933fdc, 0xbb3a792b, 0x344525bd,
0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9,
0xe01cc87e, 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7,
0x1a908749, 0xd44fbd9a, 0xd0dadecb, 0xd50ada38,
0x0339c32a, 0xc6913667, 0x8df9317c, 0xe0b12b4f,
0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c,
0xbf97222c, 0x15e6fc2a, 0x0f91fc71, 0x9b941525,
0xfae59361, 0xceb69ceb, 0xc2a86459, 0x12baa8d1,
0xb6c1075e, 0xe3056a0c, 0x10d25065, 0xcb03a442,
0xe0ec6e0e, 0x1698db3b, 0x4c98a0be, 0x3278e964,
0x9f1f9532, 0xe0d392df, 0xd3a0342b, 0x8971f21e,
0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8,
0xdf359f8d, 0x9b992f2e, 0xe60b6f47, 0x0fe3f11d,
0xe54cda54, 0x1edad891, 0xce6279cf, 0xcd3e7e6f,
0x1618b166, 0xfd2c1d05, 0x848fd2c5, 0xf6fb2299,
0xf523f357, 0xa6327623, 0x93a83531, 0x56cccd02,
0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc,
0xde966292, 0x81b949d0, 0x4c50901b, 0x71c65614,
0xe6c6c7bd, 0x327a140a, 0x45e1d006, 0xc3f27b9a,
0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 0x35bdd2f6,
0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b,
0x53113ec0, 0x1640e3d3, 0x38abbd60, 0x2547adf0,
0xba38209c, 0xf746ce76, 0x77afa1c5, 0x20756060,
0x85cbfe4e, 0x8ae88dd8, 0x7aaaf9b0, 0x4cf9aa7e,
0x1948c25c, 0x02fb8a8c, 0x01c36ae4, 0xd6ebe1f9,
0x90d4f869, 0xa65cdea0, 0x3f09252d, 0xc208e69f,
0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6,
]
]
#=============================================================================
# engine
#=============================================================================
class BlowfishEngine(object):
def __init__(self):
if BLOWFISH_P is None:
_init_constants()
self.P = list(BLOWFISH_P)
self.S = [ list(box) for box in BLOWFISH_S ]
#===================================================================
# common helpers
#===================================================================
@staticmethod
def key_to_words(data, size=18):
"""convert data to tuple of <size> 4-byte integers, repeating or
truncating data as needed to reach specified size"""
assert isinstance(data, bytes)
dlen = len(data)
if not dlen:
# return all zeros - original C code would just read the NUL after
# the password, so mimicing that behavior for this edge case.
return [0]*size
# repeat data until it fills up 4*size bytes
data = repeat_string(data, size<<2)
# unpack
return struct.unpack(">%dI" % (size,), data)
#===================================================================
# blowfish routines
#===================================================================
def encipher(self, l, r):
"loop version of blowfish encipher routine"
P, S = self.P, self.S
l ^= P[0]
i = 1
while i < 17:
# Feistel substitution on left word
r = ((((S[0][l >> 24] + S[1][(l >> 16) & 0xff]) ^ S[2][(l >> 8) & 0xff]) +
S[3][l & 0xff]) & 0xffffffff) ^ P[i] ^ r
# swap vars so even rounds do Feistel substition on right word
l, r = r, l
i += 1
return r ^ P[17], l
# NOTE: decipher is same as above, just with reversed(P) instead.
def expand(self, key_words):
"perform stock Blowfish keyschedule setup"
assert len(key_words) >= 18, "key_words must be at least as large as P"
P, S, encipher = self.P, self.S, self.encipher
i = 0
while i < 18:
P[i] ^= key_words[i]
i += 1
i = l = r = 0
while i < 18:
P[i], P[i+1] = l,r = encipher(l,r)
i += 2
for box in S:
i = 0
while i < 256:
box[i], box[i+1] = l,r = encipher(l,r)
i += 2
#===================================================================
# eks-blowfish routines
#===================================================================
def eks_salted_expand(self, key_words, salt_words):
"perform EKS' salted version of Blowfish keyschedule setup"
# NOTE: this is the same as expand(), except for the addition
# of the operations involving *salt_words*.
assert len(key_words) >= 18, "key_words must be at least as large as P"
salt_size = len(salt_words)
assert salt_size, "salt_words must not be empty"
assert not salt_size & 1, "salt_words must have even length"
P, S, encipher = self.P, self.S, self.encipher
i = 0
while i < 18:
P[i] ^= key_words[i]
i += 1
s = i = l = r = 0
while i < 18:
l ^= salt_words[s]
r ^= salt_words[s+1]
s += 2
if s == salt_size:
s = 0
P[i], P[i+1] = l,r = encipher(l,r) # next()
i += 2
for box in S:
i = 0
while i < 256:
l ^= salt_words[s]
r ^= salt_words[s+1]
s += 2
if s == salt_size:
s = 0
box[i], box[i+1] = l,r = encipher(l,r) # next()
i += 2
def eks_repeated_expand(self, key_words, salt_words, rounds):
"perform rounds stage of EKS keyschedule setup"
expand = self.expand
n = 0
while n < rounds:
expand(key_words)
expand(salt_words)
n += 1
def repeat_encipher(self, l, r, count):
"repeatedly apply encipher operation to a block"
encipher = self.encipher
n = 0
while n < count:
l, r = encipher(l, r)
n += 1
return l, r
#===================================================================
# eoc
#===================================================================
#=============================================================================
# eof
#=============================================================================

View File

@ -0,0 +1,771 @@
"""passlib.utils._blowfish.unrolled - unrolled loop implementation of bcrypt,
autogenerated by _gen_files.py
currently this override the encipher() and expand() methods
with optimized versions, and leaves the other base.py methods alone.
"""
#=============================================================================
# imports
#=============================================================================
# pkg
from passlib.utils._blowfish.base import BlowfishEngine as _BlowfishEngine
# local
__all__ = [
"BlowfishEngine",
]
#=============================================================================
#
#=============================================================================
class BlowfishEngine(_BlowfishEngine):
def encipher(self, l, r):
"""blowfish encipher a single 64-bit block encoded as two 32-bit ints"""
(p0, p1, p2, p3, p4, p5, p6, p7, p8, p9,
p10, p11, p12, p13, p14, p15, p16, p17) = self.P
S0, S1, S2, S3 = self.S
l ^= p0
# Feistel substitution on left word (round 0)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p1
# Feistel substitution on right word (round 1)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p2
# Feistel substitution on left word (round 2)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p3
# Feistel substitution on right word (round 3)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p4
# Feistel substitution on left word (round 4)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p5
# Feistel substitution on right word (round 5)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p6
# Feistel substitution on left word (round 6)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p7
# Feistel substitution on right word (round 7)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p8
# Feistel substitution on left word (round 8)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p9
# Feistel substitution on right word (round 9)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p10
# Feistel substitution on left word (round 10)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p11
# Feistel substitution on right word (round 11)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p12
# Feistel substitution on left word (round 12)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p13
# Feistel substitution on right word (round 13)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p14
# Feistel substitution on left word (round 14)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p15
# Feistel substitution on right word (round 15)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p16
return r ^ p17, l
def expand(self, key_words):
"""unrolled version of blowfish key expansion"""
##assert len(key_words) >= 18, "size of key_words must be >= 18"
P, S = self.P, self.S
S0, S1, S2, S3 = S
#=============================================================
# integrate key
#=============================================================
p0 = P[0] ^ key_words[0]
p1 = P[1] ^ key_words[1]
p2 = P[2] ^ key_words[2]
p3 = P[3] ^ key_words[3]
p4 = P[4] ^ key_words[4]
p5 = P[5] ^ key_words[5]
p6 = P[6] ^ key_words[6]
p7 = P[7] ^ key_words[7]
p8 = P[8] ^ key_words[8]
p9 = P[9] ^ key_words[9]
p10 = P[10] ^ key_words[10]
p11 = P[11] ^ key_words[11]
p12 = P[12] ^ key_words[12]
p13 = P[13] ^ key_words[13]
p14 = P[14] ^ key_words[14]
p15 = P[15] ^ key_words[15]
p16 = P[16] ^ key_words[16]
p17 = P[17] ^ key_words[17]
#=============================================================
# update P
#=============================================================
#------------------------------------------------
# update P[0] and P[1]
#------------------------------------------------
l, r = p0, 0
# Feistel substitution on left word (round 0)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p1
# Feistel substitution on right word (round 1)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p2
# Feistel substitution on left word (round 2)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p3
# Feistel substitution on right word (round 3)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p4
# Feistel substitution on left word (round 4)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p5
# Feistel substitution on right word (round 5)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p6
# Feistel substitution on left word (round 6)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p7
# Feistel substitution on right word (round 7)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p8
# Feistel substitution on left word (round 8)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p9
# Feistel substitution on right word (round 9)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p10
# Feistel substitution on left word (round 10)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p11
# Feistel substitution on right word (round 11)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p12
# Feistel substitution on left word (round 12)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p13
# Feistel substitution on right word (round 13)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p14
# Feistel substitution on left word (round 14)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p15
# Feistel substitution on right word (round 15)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p16
p0, p1 = l, r = r ^ p17, l
#------------------------------------------------
# update P[2] and P[3]
#------------------------------------------------
l ^= p0
# Feistel substitution on left word (round 0)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p1
# Feistel substitution on right word (round 1)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p2
# Feistel substitution on left word (round 2)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p3
# Feistel substitution on right word (round 3)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p4
# Feistel substitution on left word (round 4)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p5
# Feistel substitution on right word (round 5)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p6
# Feistel substitution on left word (round 6)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p7
# Feistel substitution on right word (round 7)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p8
# Feistel substitution on left word (round 8)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p9
# Feistel substitution on right word (round 9)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p10
# Feistel substitution on left word (round 10)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p11
# Feistel substitution on right word (round 11)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p12
# Feistel substitution on left word (round 12)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p13
# Feistel substitution on right word (round 13)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p14
# Feistel substitution on left word (round 14)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p15
# Feistel substitution on right word (round 15)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p16
p2, p3 = l, r = r ^ p17, l
#------------------------------------------------
# update P[4] and P[5]
#------------------------------------------------
l ^= p0
# Feistel substitution on left word (round 0)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p1
# Feistel substitution on right word (round 1)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p2
# Feistel substitution on left word (round 2)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p3
# Feistel substitution on right word (round 3)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p4
# Feistel substitution on left word (round 4)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p5
# Feistel substitution on right word (round 5)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p6
# Feistel substitution on left word (round 6)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p7
# Feistel substitution on right word (round 7)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p8
# Feistel substitution on left word (round 8)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p9
# Feistel substitution on right word (round 9)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p10
# Feistel substitution on left word (round 10)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p11
# Feistel substitution on right word (round 11)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p12
# Feistel substitution on left word (round 12)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p13
# Feistel substitution on right word (round 13)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p14
# Feistel substitution on left word (round 14)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p15
# Feistel substitution on right word (round 15)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p16
p4, p5 = l, r = r ^ p17, l
#------------------------------------------------
# update P[6] and P[7]
#------------------------------------------------
l ^= p0
# Feistel substitution on left word (round 0)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p1
# Feistel substitution on right word (round 1)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p2
# Feistel substitution on left word (round 2)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p3
# Feistel substitution on right word (round 3)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p4
# Feistel substitution on left word (round 4)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p5
# Feistel substitution on right word (round 5)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p6
# Feistel substitution on left word (round 6)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p7
# Feistel substitution on right word (round 7)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p8
# Feistel substitution on left word (round 8)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p9
# Feistel substitution on right word (round 9)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p10
# Feistel substitution on left word (round 10)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p11
# Feistel substitution on right word (round 11)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p12
# Feistel substitution on left word (round 12)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p13
# Feistel substitution on right word (round 13)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p14
# Feistel substitution on left word (round 14)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p15
# Feistel substitution on right word (round 15)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p16
p6, p7 = l, r = r ^ p17, l
#------------------------------------------------
# update P[8] and P[9]
#------------------------------------------------
l ^= p0
# Feistel substitution on left word (round 0)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p1
# Feistel substitution on right word (round 1)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p2
# Feistel substitution on left word (round 2)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p3
# Feistel substitution on right word (round 3)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p4
# Feistel substitution on left word (round 4)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p5
# Feistel substitution on right word (round 5)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p6
# Feistel substitution on left word (round 6)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p7
# Feistel substitution on right word (round 7)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p8
# Feistel substitution on left word (round 8)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p9
# Feistel substitution on right word (round 9)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p10
# Feistel substitution on left word (round 10)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p11
# Feistel substitution on right word (round 11)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p12
# Feistel substitution on left word (round 12)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p13
# Feistel substitution on right word (round 13)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p14
# Feistel substitution on left word (round 14)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p15
# Feistel substitution on right word (round 15)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p16
p8, p9 = l, r = r ^ p17, l
#------------------------------------------------
# update P[10] and P[11]
#------------------------------------------------
l ^= p0
# Feistel substitution on left word (round 0)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p1
# Feistel substitution on right word (round 1)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p2
# Feistel substitution on left word (round 2)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p3
# Feistel substitution on right word (round 3)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p4
# Feistel substitution on left word (round 4)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p5
# Feistel substitution on right word (round 5)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p6
# Feistel substitution on left word (round 6)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p7
# Feistel substitution on right word (round 7)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p8
# Feistel substitution on left word (round 8)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p9
# Feistel substitution on right word (round 9)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p10
# Feistel substitution on left word (round 10)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p11
# Feistel substitution on right word (round 11)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p12
# Feistel substitution on left word (round 12)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p13
# Feistel substitution on right word (round 13)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p14
# Feistel substitution on left word (round 14)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p15
# Feistel substitution on right word (round 15)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p16
p10, p11 = l, r = r ^ p17, l
#------------------------------------------------
# update P[12] and P[13]
#------------------------------------------------
l ^= p0
# Feistel substitution on left word (round 0)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p1
# Feistel substitution on right word (round 1)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p2
# Feistel substitution on left word (round 2)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p3
# Feistel substitution on right word (round 3)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p4
# Feistel substitution on left word (round 4)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p5
# Feistel substitution on right word (round 5)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p6
# Feistel substitution on left word (round 6)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p7
# Feistel substitution on right word (round 7)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p8
# Feistel substitution on left word (round 8)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p9
# Feistel substitution on right word (round 9)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p10
# Feistel substitution on left word (round 10)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p11
# Feistel substitution on right word (round 11)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p12
# Feistel substitution on left word (round 12)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p13
# Feistel substitution on right word (round 13)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p14
# Feistel substitution on left word (round 14)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p15
# Feistel substitution on right word (round 15)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p16
p12, p13 = l, r = r ^ p17, l
#------------------------------------------------
# update P[14] and P[15]
#------------------------------------------------
l ^= p0
# Feistel substitution on left word (round 0)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p1
# Feistel substitution on right word (round 1)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p2
# Feistel substitution on left word (round 2)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p3
# Feistel substitution on right word (round 3)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p4
# Feistel substitution on left word (round 4)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p5
# Feistel substitution on right word (round 5)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p6
# Feistel substitution on left word (round 6)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p7
# Feistel substitution on right word (round 7)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p8
# Feistel substitution on left word (round 8)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p9
# Feistel substitution on right word (round 9)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p10
# Feistel substitution on left word (round 10)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p11
# Feistel substitution on right word (round 11)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p12
# Feistel substitution on left word (round 12)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p13
# Feistel substitution on right word (round 13)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p14
# Feistel substitution on left word (round 14)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p15
# Feistel substitution on right word (round 15)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p16
p14, p15 = l, r = r ^ p17, l
#------------------------------------------------
# update P[16] and P[17]
#------------------------------------------------
l ^= p0
# Feistel substitution on left word (round 0)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p1
# Feistel substitution on right word (round 1)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p2
# Feistel substitution on left word (round 2)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p3
# Feistel substitution on right word (round 3)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p4
# Feistel substitution on left word (round 4)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p5
# Feistel substitution on right word (round 5)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p6
# Feistel substitution on left word (round 6)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p7
# Feistel substitution on right word (round 7)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p8
# Feistel substitution on left word (round 8)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p9
# Feistel substitution on right word (round 9)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p10
# Feistel substitution on left word (round 10)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p11
# Feistel substitution on right word (round 11)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p12
# Feistel substitution on left word (round 12)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p13
# Feistel substitution on right word (round 13)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p14
# Feistel substitution on left word (round 14)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p15
# Feistel substitution on right word (round 15)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p16
p16, p17 = l, r = r ^ p17, l
#------------------------------------------------
# save changes to original P array
#------------------------------------------------
P[:] = (p0, p1, p2, p3, p4, p5, p6, p7, p8, p9,
p10, p11, p12, p13, p14, p15, p16, p17)
#=============================================================
# update S
#=============================================================
for box in S:
j = 0
while j < 256:
l ^= p0
# Feistel substitution on left word (round 0)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p1
# Feistel substitution on right word (round 1)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p2
# Feistel substitution on left word (round 2)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p3
# Feistel substitution on right word (round 3)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p4
# Feistel substitution on left word (round 4)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p5
# Feistel substitution on right word (round 5)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p6
# Feistel substitution on left word (round 6)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p7
# Feistel substitution on right word (round 7)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p8
# Feistel substitution on left word (round 8)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p9
# Feistel substitution on right word (round 9)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p10
# Feistel substitution on left word (round 10)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p11
# Feistel substitution on right word (round 11)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p12
# Feistel substitution on left word (round 12)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p13
# Feistel substitution on right word (round 13)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p14
# Feistel substitution on left word (round 14)
r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) +
S3[l & 0xff]) & 0xffffffff) ^ p15
# Feistel substitution on right word (round 15)
l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) +
S3[r & 0xff]) & 0xffffffff) ^ p16
box[j], box[j+1] = l, r = r ^ p17, l
j += 2
#===================================================================
# eoc
#===================================================================
#=============================================================================
# eof
#=============================================================================

436
passlib/utils/compat.py Normal file
View File

@ -0,0 +1,436 @@
"""passlib.utils.compat - python 2/3 compatibility helpers"""
#=============================================================================
# figure out what we're running
#=============================================================================
#------------------------------------------------------------------------
# python version
#------------------------------------------------------------------------
import sys
PY2 = sys.version_info < (3,0)
PY3 = sys.version_info >= (3,0)
PY_MAX_25 = sys.version_info < (2,6) # py 2.5 or earlier
PY27 = sys.version_info[:2] == (2,7) # supports last 2.x release
PY_MIN_32 = sys.version_info >= (3,2) # py 3.2 or later
#------------------------------------------------------------------------
# python implementation
#------------------------------------------------------------------------
PYPY = hasattr(sys, "pypy_version_info")
JYTHON = sys.platform.startswith('java')
#------------------------------------------------------------------------
# capabilities
#------------------------------------------------------------------------
# __dir__() added in py2.6
SUPPORTS_DIR_METHOD = not PY_MAX_25 and not (PYPY and sys.pypy_version_info < (1,6))
#=============================================================================
# common imports
#=============================================================================
import logging; log = logging.getLogger(__name__)
if PY3:
import builtins
else:
import __builtin__ as builtins
def add_doc(obj, doc):
"""add docstring to an object"""
obj.__doc__ = doc
#=============================================================================
# the default exported vars
#=============================================================================
__all__ = [
# python versions
'PY2', 'PY3', 'PY_MAX_25', 'PY27', 'PY_MIN_32',
# io
'BytesIO', 'StringIO', 'NativeStringIO', 'SafeConfigParser',
'print_',
# type detection
## 'is_mapping',
'callable',
'int_types',
'num_types',
'base_string_types',
# unicode/bytes types & helpers
'u', 'b',
'unicode', 'bytes',
'uascii_to_str', 'bascii_to_str',
'str_to_uascii', 'str_to_bascii',
'join_unicode', 'join_bytes',
'join_byte_values', 'join_byte_elems',
'byte_elem_value',
'iter_byte_values',
# iteration helpers
'irange', #'lrange',
'imap', 'lmap',
'iteritems', 'itervalues',
'next',
# introspection
'exc_err', 'get_method_function', 'add_doc',
]
# begin accumulating mapping of lazy-loaded attrs,
# 'merged' into module at bottom
_lazy_attrs = dict()
#=============================================================================
# unicode & bytes types
#=============================================================================
if PY3:
unicode = str
bytes = builtins.bytes
def u(s):
assert isinstance(s, str)
return s
def b(s):
assert isinstance(s, str)
return s.encode("latin-1")
base_string_types = (unicode, bytes)
else:
unicode = builtins.unicode
bytes = str if PY_MAX_25 else builtins.bytes
def u(s):
assert isinstance(s, str)
return s.decode("unicode_escape")
def b(s):
assert isinstance(s, str)
return s
base_string_types = basestring
#=============================================================================
# unicode & bytes helpers
#=============================================================================
# function to join list of unicode strings
join_unicode = u('').join
# function to join list of byte strings
join_bytes = b('').join
if PY3:
def uascii_to_str(s):
assert isinstance(s, unicode)
return s
def bascii_to_str(s):
assert isinstance(s, bytes)
return s.decode("ascii")
def str_to_uascii(s):
assert isinstance(s, str)
return s
def str_to_bascii(s):
assert isinstance(s, str)
return s.encode("ascii")
join_byte_values = join_byte_elems = bytes
def byte_elem_value(elem):
assert isinstance(elem, int)
return elem
def iter_byte_values(s):
assert isinstance(s, bytes)
return s
def iter_byte_chars(s):
assert isinstance(s, bytes)
# FIXME: there has to be a better way to do this
return (bytes([c]) for c in s)
else:
def uascii_to_str(s):
assert isinstance(s, unicode)
return s.encode("ascii")
def bascii_to_str(s):
assert isinstance(s, bytes)
return s
def str_to_uascii(s):
assert isinstance(s, str)
return s.decode("ascii")
def str_to_bascii(s):
assert isinstance(s, str)
return s
def join_byte_values(values):
return join_bytes(chr(v) for v in values)
join_byte_elems = join_bytes
byte_elem_value = ord
def iter_byte_values(s):
assert isinstance(s, bytes)
return (ord(c) for c in s)
def iter_byte_chars(s):
assert isinstance(s, bytes)
return s
add_doc(uascii_to_str, "helper to convert ascii unicode -> native str")
add_doc(bascii_to_str, "helper to convert ascii bytes -> native str")
add_doc(str_to_uascii, "helper to convert ascii native str -> unicode")
add_doc(str_to_bascii, "helper to convert ascii native str -> bytes")
# join_byte_values -- function to convert list of ordinal integers to byte string.
# join_byte_elems -- function to convert list of byte elements to byte string;
# i.e. what's returned by ``b('a')[0]``...
# this is b('a') under PY2, but 97 under PY3.
# byte_elem_value -- function to convert byte element to integer -- a noop under PY3
add_doc(iter_byte_values, "iterate over byte string as sequence of ints 0-255")
add_doc(iter_byte_chars, "iterate over byte string as sequence of 1-byte strings")
#=============================================================================
# numeric
#=============================================================================
if PY3:
int_types = (int,)
num_types = (int, float)
else:
int_types = (int, long)
num_types = (int, long, float)
#=============================================================================
# iteration helpers
#
# irange - range iterable / view (xrange under py2, range under py3)
# lrange - range list (range under py2, list(range()) under py3)
#
# imap - map to iterator
# lmap - map to list
#=============================================================================
if PY3:
irange = range
##def lrange(*a,**k):
## return list(range(*a,**k))
def lmap(*a, **k):
return list(map(*a,**k))
imap = map
def iteritems(d):
return d.items()
def itervalues(d):
return d.values()
next_method_attr = "__next__"
else:
irange = xrange
##lrange = range
lmap = map
from itertools import imap
def iteritems(d):
return d.iteritems()
def itervalues(d):
return d.itervalues()
next_method_attr = "next"
if PY_MAX_25:
_undef = object()
def next(itr, default=_undef):
"compat wrapper for next()"
if default is _undef:
return itr.next()
try:
return itr.next()
except StopIteration:
return default
else:
next = builtins.next
#=============================================================================
# typing
#=============================================================================
##def is_mapping(obj):
## # non-exhaustive check, enough to distinguish from lists, etc
## return hasattr(obj, "items")
if (3,0) <= sys.version_info < (3,2):
# callable isn't dead, it's just resting
from collections import Callable
def callable(obj):
return isinstance(obj, Callable)
else:
callable = builtins.callable
#=============================================================================
# introspection
#=============================================================================
def exc_err():
"return current error object (to avoid try/except syntax change)"
return sys.exc_info()[1]
if PY3:
method_function_attr = "__func__"
else:
method_function_attr = "im_func"
def get_method_function(func):
"given (potential) method, return underlying function"
return getattr(func, method_function_attr, func)
#=============================================================================
# input/output
#=============================================================================
if PY3:
_lazy_attrs = dict(
BytesIO="io.BytesIO",
UnicodeIO="io.StringIO",
NativeStringIO="io.StringIO",
SafeConfigParser="configparser.SafeConfigParser",
)
if sys.version_info >= (3,2):
# py32 renamed this, removing old ConfigParser
_lazy_attrs["SafeConfigParser"] = "configparser.ConfigParser"
print_ = getattr(builtins, "print")
else:
_lazy_attrs = dict(
BytesIO="cStringIO.StringIO",
UnicodeIO="StringIO.StringIO",
NativeStringIO="cStringIO.StringIO",
SafeConfigParser="ConfigParser.SafeConfigParser",
)
def print_(*args, **kwds):
"""The new-style print function."""
# extract kwd args
fp = kwds.pop("file", sys.stdout)
sep = kwds.pop("sep", None)
end = kwds.pop("end", None)
if kwds:
raise TypeError("invalid keyword arguments")
# short-circuit if no target
if fp is None:
return
# use unicode or bytes ?
want_unicode = isinstance(sep, unicode) or isinstance(end, unicode) or \
any(isinstance(arg, unicode) for arg in args)
# pick default end sequence
if end is None:
end = u("\n") if want_unicode else "\n"
elif not isinstance(end, base_string_types):
raise TypeError("end must be None or a string")
# pick default separator
if sep is None:
sep = u(" ") if want_unicode else " "
elif not isinstance(sep, base_string_types):
raise TypeError("sep must be None or a string")
# write to buffer
first = True
write = fp.write
for arg in args:
if first:
first = False
else:
write(sep)
if not isinstance(arg, basestring):
arg = str(arg)
write(arg)
write(end)
#=============================================================================
# lazy overlay module
#=============================================================================
from types import ModuleType
def _import_object(source):
"helper to import object from module; accept format `path.to.object`"
modname, modattr = source.rsplit(".",1)
mod = __import__(modname, fromlist=[modattr], level=0)
return getattr(mod, modattr)
class _LazyOverlayModule(ModuleType):
"""proxy module which overlays original module,
and lazily imports specified attributes.
this is mainly used to prevent importing of resources
that are only needed by certain password hashes,
yet allow them to be imported from a single location.
used by :mod:`passlib.utils`, :mod:`passlib.utils.crypto`,
and :mod:`passlib.utils.compat`.
"""
@classmethod
def replace_module(cls, name, attrmap):
orig = sys.modules[name]
self = cls(name, attrmap, orig)
sys.modules[name] = self
return self
def __init__(self, name, attrmap, proxy=None):
ModuleType.__init__(self, name)
self.__attrmap = attrmap
self.__proxy = proxy
self.__log = logging.getLogger(name)
def __getattr__(self, attr):
proxy = self.__proxy
if proxy and hasattr(proxy, attr):
return getattr(proxy, attr)
attrmap = self.__attrmap
if attr in attrmap:
source = attrmap[attr]
if callable(source):
value = source()
else:
value = _import_object(source)
setattr(self, attr, value)
self.__log.debug("loaded lazy attr %r: %r", attr, value)
return value
raise AttributeError("'module' object has no attribute '%s'" % (attr,))
def __repr__(self):
proxy = self.__proxy
if proxy:
return repr(proxy)
else:
return ModuleType.__repr__(self)
def __dir__(self):
attrs = set(dir(self.__class__))
attrs.update(self.__dict__)
attrs.update(self.__attrmap)
proxy = self.__proxy
if proxy is not None:
attrs.update(dir(proxy))
return list(attrs)
# replace this module with overlay that will lazily import attributes.
_LazyOverlayModule.replace_module(__name__, _lazy_attrs)
#=============================================================================
# eof
#=============================================================================

859
passlib/utils/des.py Normal file
View File

@ -0,0 +1,859 @@
"""passlib.utils.des -- DES block encryption routines
History
=======
These routines (which have since been drastically modified for python)
are based on a Java implementation of the des-crypt algorithm,
found at `<http://www.dynamic.net.au/christos/crypt/UnixCrypt2.txt>`_.
The copyright & license for that source is as follows::
UnixCrypt.java 0.9 96/11/25
Copyright (c) 1996 Aki Yoshida. All rights reserved.
Permission to use, copy, modify and distribute this software
for non-commercial or commercial purposes and without fee is
hereby granted provided that this copyright notice appears in
all copies.
---
Unix crypt(3C) utility
@version 0.9, 11/25/96
@author Aki Yoshida
---
modified April 2001
by Iris Van den Broeke, Daniel Deville
---
Unix Crypt.
Implements the one way cryptography used by Unix systems for
simple password protection.
@version $Id: UnixCrypt2.txt,v 1.1.1.1 2005/09/13 22:20:13 christos Exp $
@author Greg Wilkins (gregw)
The netbsd des-crypt implementation has some nice notes on how this all works -
http://fxr.googlebit.com/source/lib/libcrypt/crypt.c?v=NETBSD-CURRENT
"""
# TODO: could use an accelerated C version of this module to speed up lmhash,
# des-crypt, and ext-des-crypt
#=============================================================================
# imports
#=============================================================================
# core
import struct
# pkg
from passlib import exc
from passlib.utils.compat import bytes, join_byte_values, byte_elem_value, \
b, irange, irange, int_types
from passlib.utils import deprecated_function
# local
__all__ = [
"expand_des_key",
"des_encrypt_block",
"mdes_encrypt_int_block",
]
#=============================================================================
# constants
#=============================================================================
# masks/upper limits for various integer sizes
INT_24_MASK = 0xffffff
INT_56_MASK = 0xffffffffffffff
INT_64_MASK = 0xffffffffffffffff
# mask to clear parity bits from 64-bit key
_KDATA_MASK = 0xfefefefefefefefe
_KPARITY_MASK = 0x0101010101010101
# mask used to setup key schedule
_KS_MASK = 0xfcfcfcfcffffffff
#=============================================================================
# static DES tables
#=============================================================================
# placeholders filled in by _load_tables()
PCXROT = IE3264 = SPE = CF6464 = None
def _load_tables():
"delay loading tables until they are actually needed"
global PCXROT, IE3264, SPE, CF6464
#---------------------------------------------------------------
# Initial key schedule permutation
# PC1ROT - bit reverse, then PC1, then Rotate, then PC2
#---------------------------------------------------------------
# NOTE: this was reordered from original table to make perm3264 logic simpler
PC1ROT=(
( 0x0000000000000000, 0x0000000000000000, 0x0000000000002000, 0x0000000000002000,
0x0000000000000020, 0x0000000000000020, 0x0000000000002020, 0x0000000000002020,
0x0000000000000400, 0x0000000000000400, 0x0000000000002400, 0x0000000000002400,
0x0000000000000420, 0x0000000000000420, 0x0000000000002420, 0x0000000000002420, ),
( 0x0000000000000000, 0x2000000000000000, 0x0000000400000000, 0x2000000400000000,
0x0000800000000000, 0x2000800000000000, 0x0000800400000000, 0x2000800400000000,
0x0008000000000000, 0x2008000000000000, 0x0008000400000000, 0x2008000400000000,
0x0008800000000000, 0x2008800000000000, 0x0008800400000000, 0x2008800400000000, ),
( 0x0000000000000000, 0x0000000000000000, 0x0000000000000040, 0x0000000000000040,
0x0000000020000000, 0x0000000020000000, 0x0000000020000040, 0x0000000020000040,
0x0000000000200000, 0x0000000000200000, 0x0000000000200040, 0x0000000000200040,
0x0000000020200000, 0x0000000020200000, 0x0000000020200040, 0x0000000020200040, ),
( 0x0000000000000000, 0x0002000000000000, 0x0800000000000000, 0x0802000000000000,
0x0100000000000000, 0x0102000000000000, 0x0900000000000000, 0x0902000000000000,
0x4000000000000000, 0x4002000000000000, 0x4800000000000000, 0x4802000000000000,
0x4100000000000000, 0x4102000000000000, 0x4900000000000000, 0x4902000000000000, ),
( 0x0000000000000000, 0x0000000000000000, 0x0000000000040000, 0x0000000000040000,
0x0000020000000000, 0x0000020000000000, 0x0000020000040000, 0x0000020000040000,
0x0000000000000004, 0x0000000000000004, 0x0000000000040004, 0x0000000000040004,
0x0000020000000004, 0x0000020000000004, 0x0000020000040004, 0x0000020000040004, ),
( 0x0000000000000000, 0x0000400000000000, 0x0200000000000000, 0x0200400000000000,
0x0080000000000000, 0x0080400000000000, 0x0280000000000000, 0x0280400000000000,
0x0000008000000000, 0x0000408000000000, 0x0200008000000000, 0x0200408000000000,
0x0080008000000000, 0x0080408000000000, 0x0280008000000000, 0x0280408000000000, ),
( 0x0000000000000000, 0x0000000000000000, 0x0000000010000000, 0x0000000010000000,
0x0000000000001000, 0x0000000000001000, 0x0000000010001000, 0x0000000010001000,
0x0000000040000000, 0x0000000040000000, 0x0000000050000000, 0x0000000050000000,
0x0000000040001000, 0x0000000040001000, 0x0000000050001000, 0x0000000050001000, ),
( 0x0000000000000000, 0x0000001000000000, 0x0000080000000000, 0x0000081000000000,
0x1000000000000000, 0x1000001000000000, 0x1000080000000000, 0x1000081000000000,
0x0004000000000000, 0x0004001000000000, 0x0004080000000000, 0x0004081000000000,
0x1004000000000000, 0x1004001000000000, 0x1004080000000000, 0x1004081000000000, ),
( 0x0000000000000000, 0x0000000000000000, 0x0000000000000080, 0x0000000000000080,
0x0000000000080000, 0x0000000000080000, 0x0000000000080080, 0x0000000000080080,
0x0000000000800000, 0x0000000000800000, 0x0000000000800080, 0x0000000000800080,
0x0000000000880000, 0x0000000000880000, 0x0000000000880080, 0x0000000000880080, ),
( 0x0000000000000000, 0x0000000008000000, 0x0000002000000000, 0x0000002008000000,
0x0000100000000000, 0x0000100008000000, 0x0000102000000000, 0x0000102008000000,
0x0000200000000000, 0x0000200008000000, 0x0000202000000000, 0x0000202008000000,
0x0000300000000000, 0x0000300008000000, 0x0000302000000000, 0x0000302008000000, ),
( 0x0000000000000000, 0x0000000000000000, 0x0000000000400000, 0x0000000000400000,
0x0000000004000000, 0x0000000004000000, 0x0000000004400000, 0x0000000004400000,
0x0000000000000800, 0x0000000000000800, 0x0000000000400800, 0x0000000000400800,
0x0000000004000800, 0x0000000004000800, 0x0000000004400800, 0x0000000004400800, ),
( 0x0000000000000000, 0x0000000000008000, 0x0040000000000000, 0x0040000000008000,
0x0000004000000000, 0x0000004000008000, 0x0040004000000000, 0x0040004000008000,
0x8000000000000000, 0x8000000000008000, 0x8040000000000000, 0x8040000000008000,
0x8000004000000000, 0x8000004000008000, 0x8040004000000000, 0x8040004000008000, ),
( 0x0000000000000000, 0x0000000000000000, 0x0000000000004000, 0x0000000000004000,
0x0000000000000008, 0x0000000000000008, 0x0000000000004008, 0x0000000000004008,
0x0000000000000010, 0x0000000000000010, 0x0000000000004010, 0x0000000000004010,
0x0000000000000018, 0x0000000000000018, 0x0000000000004018, 0x0000000000004018, ),
( 0x0000000000000000, 0x0000000200000000, 0x0001000000000000, 0x0001000200000000,
0x0400000000000000, 0x0400000200000000, 0x0401000000000000, 0x0401000200000000,
0x0020000000000000, 0x0020000200000000, 0x0021000000000000, 0x0021000200000000,
0x0420000000000000, 0x0420000200000000, 0x0421000000000000, 0x0421000200000000, ),
( 0x0000000000000000, 0x0000000000000000, 0x0000010000000000, 0x0000010000000000,
0x0000000100000000, 0x0000000100000000, 0x0000010100000000, 0x0000010100000000,
0x0000000000100000, 0x0000000000100000, 0x0000010000100000, 0x0000010000100000,
0x0000000100100000, 0x0000000100100000, 0x0000010100100000, 0x0000010100100000, ),
( 0x0000000000000000, 0x0000000080000000, 0x0000040000000000, 0x0000040080000000,
0x0010000000000000, 0x0010000080000000, 0x0010040000000000, 0x0010040080000000,
0x0000000800000000, 0x0000000880000000, 0x0000040800000000, 0x0000040880000000,
0x0010000800000000, 0x0010000880000000, 0x0010040800000000, 0x0010040880000000, ),
)
#---------------------------------------------------------------
# Subsequent key schedule rotation permutations
# PC2ROT - PC2 inverse, then Rotate, then PC2
#---------------------------------------------------------------
# NOTE: this was reordered from original table to make perm3264 logic simpler
PC2ROTA=(
( 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000,
0x0000000000200000, 0x0000000000200000, 0x0000000000200000, 0x0000000000200000,
0x0000000004000000, 0x0000000004000000, 0x0000000004000000, 0x0000000004000000,
0x0000000004200000, 0x0000000004200000, 0x0000000004200000, 0x0000000004200000, ),
( 0x0000000000000000, 0x0000000000000800, 0x0000010000000000, 0x0000010000000800,
0x0000000000002000, 0x0000000000002800, 0x0000010000002000, 0x0000010000002800,
0x0000000010000000, 0x0000000010000800, 0x0000010010000000, 0x0000010010000800,
0x0000000010002000, 0x0000000010002800, 0x0000010010002000, 0x0000010010002800, ),
( 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000,
0x0000000100000000, 0x0000000100000000, 0x0000000100000000, 0x0000000100000000,
0x0000000000800000, 0x0000000000800000, 0x0000000000800000, 0x0000000000800000,
0x0000000100800000, 0x0000000100800000, 0x0000000100800000, 0x0000000100800000, ),
( 0x0000000000000000, 0x0000020000000000, 0x0000000080000000, 0x0000020080000000,
0x0000000000400000, 0x0000020000400000, 0x0000000080400000, 0x0000020080400000,
0x0000000008000000, 0x0000020008000000, 0x0000000088000000, 0x0000020088000000,
0x0000000008400000, 0x0000020008400000, 0x0000000088400000, 0x0000020088400000, ),
( 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000,
0x0000000000000040, 0x0000000000000040, 0x0000000000000040, 0x0000000000000040,
0x0000000000001000, 0x0000000000001000, 0x0000000000001000, 0x0000000000001000,
0x0000000000001040, 0x0000000000001040, 0x0000000000001040, 0x0000000000001040, ),
( 0x0000000000000000, 0x0000000000000010, 0x0000000000000400, 0x0000000000000410,
0x0000000000000080, 0x0000000000000090, 0x0000000000000480, 0x0000000000000490,
0x0000000040000000, 0x0000000040000010, 0x0000000040000400, 0x0000000040000410,
0x0000000040000080, 0x0000000040000090, 0x0000000040000480, 0x0000000040000490, ),
( 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000,
0x0000000000080000, 0x0000000000080000, 0x0000000000080000, 0x0000000000080000,
0x0000000000100000, 0x0000000000100000, 0x0000000000100000, 0x0000000000100000,
0x0000000000180000, 0x0000000000180000, 0x0000000000180000, 0x0000000000180000, ),
( 0x0000000000000000, 0x0000000000040000, 0x0000000000000020, 0x0000000000040020,
0x0000000000000004, 0x0000000000040004, 0x0000000000000024, 0x0000000000040024,
0x0000000200000000, 0x0000000200040000, 0x0000000200000020, 0x0000000200040020,
0x0000000200000004, 0x0000000200040004, 0x0000000200000024, 0x0000000200040024, ),
( 0x0000000000000000, 0x0000000000000008, 0x0000000000008000, 0x0000000000008008,
0x0010000000000000, 0x0010000000000008, 0x0010000000008000, 0x0010000000008008,
0x0020000000000000, 0x0020000000000008, 0x0020000000008000, 0x0020000000008008,
0x0030000000000000, 0x0030000000000008, 0x0030000000008000, 0x0030000000008008, ),
( 0x0000000000000000, 0x0000400000000000, 0x0000080000000000, 0x0000480000000000,
0x0000100000000000, 0x0000500000000000, 0x0000180000000000, 0x0000580000000000,
0x4000000000000000, 0x4000400000000000, 0x4000080000000000, 0x4000480000000000,
0x4000100000000000, 0x4000500000000000, 0x4000180000000000, 0x4000580000000000, ),
( 0x0000000000000000, 0x0000000000004000, 0x0000000020000000, 0x0000000020004000,
0x0001000000000000, 0x0001000000004000, 0x0001000020000000, 0x0001000020004000,
0x0200000000000000, 0x0200000000004000, 0x0200000020000000, 0x0200000020004000,
0x0201000000000000, 0x0201000000004000, 0x0201000020000000, 0x0201000020004000, ),
( 0x0000000000000000, 0x1000000000000000, 0x0004000000000000, 0x1004000000000000,
0x0002000000000000, 0x1002000000000000, 0x0006000000000000, 0x1006000000000000,
0x0000000800000000, 0x1000000800000000, 0x0004000800000000, 0x1004000800000000,
0x0002000800000000, 0x1002000800000000, 0x0006000800000000, 0x1006000800000000, ),
( 0x0000000000000000, 0x0040000000000000, 0x2000000000000000, 0x2040000000000000,
0x0000008000000000, 0x0040008000000000, 0x2000008000000000, 0x2040008000000000,
0x0000001000000000, 0x0040001000000000, 0x2000001000000000, 0x2040001000000000,
0x0000009000000000, 0x0040009000000000, 0x2000009000000000, 0x2040009000000000, ),
( 0x0000000000000000, 0x0400000000000000, 0x8000000000000000, 0x8400000000000000,
0x0000002000000000, 0x0400002000000000, 0x8000002000000000, 0x8400002000000000,
0x0100000000000000, 0x0500000000000000, 0x8100000000000000, 0x8500000000000000,
0x0100002000000000, 0x0500002000000000, 0x8100002000000000, 0x8500002000000000, ),
( 0x0000000000000000, 0x0000800000000000, 0x0800000000000000, 0x0800800000000000,
0x0000004000000000, 0x0000804000000000, 0x0800004000000000, 0x0800804000000000,
0x0000000400000000, 0x0000800400000000, 0x0800000400000000, 0x0800800400000000,
0x0000004400000000, 0x0000804400000000, 0x0800004400000000, 0x0800804400000000, ),
( 0x0000000000000000, 0x0080000000000000, 0x0000040000000000, 0x0080040000000000,
0x0008000000000000, 0x0088000000000000, 0x0008040000000000, 0x0088040000000000,
0x0000200000000000, 0x0080200000000000, 0x0000240000000000, 0x0080240000000000,
0x0008200000000000, 0x0088200000000000, 0x0008240000000000, 0x0088240000000000, ),
)
# NOTE: this was reordered from original table to make perm3264 logic simpler
PC2ROTB=(
( 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000,
0x0000000000000400, 0x0000000000000400, 0x0000000000000400, 0x0000000000000400,
0x0000000000080000, 0x0000000000080000, 0x0000000000080000, 0x0000000000080000,
0x0000000000080400, 0x0000000000080400, 0x0000000000080400, 0x0000000000080400, ),
( 0x0000000000000000, 0x0000000000800000, 0x0000000000004000, 0x0000000000804000,
0x0000000080000000, 0x0000000080800000, 0x0000000080004000, 0x0000000080804000,
0x0000000000040000, 0x0000000000840000, 0x0000000000044000, 0x0000000000844000,
0x0000000080040000, 0x0000000080840000, 0x0000000080044000, 0x0000000080844000, ),
( 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000,
0x0000000000000008, 0x0000000000000008, 0x0000000000000008, 0x0000000000000008,
0x0000000040000000, 0x0000000040000000, 0x0000000040000000, 0x0000000040000000,
0x0000000040000008, 0x0000000040000008, 0x0000000040000008, 0x0000000040000008, ),
( 0x0000000000000000, 0x0000000020000000, 0x0000000200000000, 0x0000000220000000,
0x0000000000000080, 0x0000000020000080, 0x0000000200000080, 0x0000000220000080,
0x0000000000100000, 0x0000000020100000, 0x0000000200100000, 0x0000000220100000,
0x0000000000100080, 0x0000000020100080, 0x0000000200100080, 0x0000000220100080, ),
( 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000,
0x0000000000002000, 0x0000000000002000, 0x0000000000002000, 0x0000000000002000,
0x0000020000000000, 0x0000020000000000, 0x0000020000000000, 0x0000020000000000,
0x0000020000002000, 0x0000020000002000, 0x0000020000002000, 0x0000020000002000, ),
( 0x0000000000000000, 0x0000000000000800, 0x0000000100000000, 0x0000000100000800,
0x0000000010000000, 0x0000000010000800, 0x0000000110000000, 0x0000000110000800,
0x0000000000000004, 0x0000000000000804, 0x0000000100000004, 0x0000000100000804,
0x0000000010000004, 0x0000000010000804, 0x0000000110000004, 0x0000000110000804, ),
( 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000,
0x0000000000001000, 0x0000000000001000, 0x0000000000001000, 0x0000000000001000,
0x0000000000000010, 0x0000000000000010, 0x0000000000000010, 0x0000000000000010,
0x0000000000001010, 0x0000000000001010, 0x0000000000001010, 0x0000000000001010, ),
( 0x0000000000000000, 0x0000000000000040, 0x0000010000000000, 0x0000010000000040,
0x0000000000200000, 0x0000000000200040, 0x0000010000200000, 0x0000010000200040,
0x0000000000008000, 0x0000000000008040, 0x0000010000008000, 0x0000010000008040,
0x0000000000208000, 0x0000000000208040, 0x0000010000208000, 0x0000010000208040, ),
( 0x0000000000000000, 0x0000000004000000, 0x0000000008000000, 0x000000000c000000,
0x0400000000000000, 0x0400000004000000, 0x0400000008000000, 0x040000000c000000,
0x8000000000000000, 0x8000000004000000, 0x8000000008000000, 0x800000000c000000,
0x8400000000000000, 0x8400000004000000, 0x8400000008000000, 0x840000000c000000, ),
( 0x0000000000000000, 0x0002000000000000, 0x0200000000000000, 0x0202000000000000,
0x1000000000000000, 0x1002000000000000, 0x1200000000000000, 0x1202000000000000,
0x0008000000000000, 0x000a000000000000, 0x0208000000000000, 0x020a000000000000,
0x1008000000000000, 0x100a000000000000, 0x1208000000000000, 0x120a000000000000, ),
( 0x0000000000000000, 0x0000000000400000, 0x0000000000000020, 0x0000000000400020,
0x0040000000000000, 0x0040000000400000, 0x0040000000000020, 0x0040000000400020,
0x0800000000000000, 0x0800000000400000, 0x0800000000000020, 0x0800000000400020,
0x0840000000000000, 0x0840000000400000, 0x0840000000000020, 0x0840000000400020, ),
( 0x0000000000000000, 0x0080000000000000, 0x0000008000000000, 0x0080008000000000,
0x2000000000000000, 0x2080000000000000, 0x2000008000000000, 0x2080008000000000,
0x0020000000000000, 0x00a0000000000000, 0x0020008000000000, 0x00a0008000000000,
0x2020000000000000, 0x20a0000000000000, 0x2020008000000000, 0x20a0008000000000, ),
( 0x0000000000000000, 0x0000002000000000, 0x0000040000000000, 0x0000042000000000,
0x4000000000000000, 0x4000002000000000, 0x4000040000000000, 0x4000042000000000,
0x0000400000000000, 0x0000402000000000, 0x0000440000000000, 0x0000442000000000,
0x4000400000000000, 0x4000402000000000, 0x4000440000000000, 0x4000442000000000, ),
( 0x0000000000000000, 0x0000004000000000, 0x0000200000000000, 0x0000204000000000,
0x0000080000000000, 0x0000084000000000, 0x0000280000000000, 0x0000284000000000,
0x0000800000000000, 0x0000804000000000, 0x0000a00000000000, 0x0000a04000000000,
0x0000880000000000, 0x0000884000000000, 0x0000a80000000000, 0x0000a84000000000, ),
( 0x0000000000000000, 0x0000000800000000, 0x0000000400000000, 0x0000000c00000000,
0x0000100000000000, 0x0000100800000000, 0x0000100400000000, 0x0000100c00000000,
0x0010000000000000, 0x0010000800000000, 0x0010000400000000, 0x0010000c00000000,
0x0010100000000000, 0x0010100800000000, 0x0010100400000000, 0x0010100c00000000, ),
( 0x0000000000000000, 0x0100000000000000, 0x0001000000000000, 0x0101000000000000,
0x0000001000000000, 0x0100001000000000, 0x0001001000000000, 0x0101001000000000,
0x0004000000000000, 0x0104000000000000, 0x0005000000000000, 0x0105000000000000,
0x0004001000000000, 0x0104001000000000, 0x0005001000000000, 0x0105001000000000, ),
)
#---------------------------------------------------------------
# PCXROT - PC1ROT, PC2ROTA, PC2ROTB listed in order
# of the PC1 rotation schedule, as used by des_setkey
#---------------------------------------------------------------
##ROTATES = (1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1)
##PCXROT = (
## PC1ROT, PC2ROTA, PC2ROTB, PC2ROTB,
## PC2ROTB, PC2ROTB, PC2ROTB, PC2ROTB,
## PC2ROTA, PC2ROTB, PC2ROTB, PC2ROTB,
## PC2ROTB, PC2ROTB, PC2ROTB, PC2ROTA,
## )
# NOTE: modified PCXROT to contain entrys broken into pairs,
# to help generate them in format best used by encoder.
PCXROT = (
(PC1ROT, PC2ROTA), (PC2ROTB, PC2ROTB),
(PC2ROTB, PC2ROTB), (PC2ROTB, PC2ROTB),
(PC2ROTA, PC2ROTB), (PC2ROTB, PC2ROTB),
(PC2ROTB, PC2ROTB), (PC2ROTB, PC2ROTA),
)
#---------------------------------------------------------------
# Bit reverse, intial permupation, expantion
# Initial permutation/expansion table
#---------------------------------------------------------------
# NOTE: this was reordered from original table to make perm3264 logic simpler
IE3264=(
( 0x0000000000000000, 0x0000000000800800, 0x0000000000008008, 0x0000000000808808,
0x0000008008000000, 0x0000008008800800, 0x0000008008008008, 0x0000008008808808,
0x0000000080080000, 0x0000000080880800, 0x0000000080088008, 0x0000000080888808,
0x0000008088080000, 0x0000008088880800, 0x0000008088088008, 0x0000008088888808, ),
( 0x0000000000000000, 0x0080080000000000, 0x0000800800000000, 0x0080880800000000,
0x0800000000000080, 0x0880080000000080, 0x0800800800000080, 0x0880880800000080,
0x8008000000000000, 0x8088080000000000, 0x8008800800000000, 0x8088880800000000,
0x8808000000000080, 0x8888080000000080, 0x8808800800000080, 0x8888880800000080, ),
( 0x0000000000000000, 0x0000000000001000, 0x0000000000000010, 0x0000000000001010,
0x0000000010000000, 0x0000000010001000, 0x0000000010000010, 0x0000000010001010,
0x0000000000100000, 0x0000000000101000, 0x0000000000100010, 0x0000000000101010,
0x0000000010100000, 0x0000000010101000, 0x0000000010100010, 0x0000000010101010, ),
( 0x0000000000000000, 0x0000100000000000, 0x0000001000000000, 0x0000101000000000,
0x1000000000000000, 0x1000100000000000, 0x1000001000000000, 0x1000101000000000,
0x0010000000000000, 0x0010100000000000, 0x0010001000000000, 0x0010101000000000,
0x1010000000000000, 0x1010100000000000, 0x1010001000000000, 0x1010101000000000, ),
( 0x0000000000000000, 0x0000000000002000, 0x0000000000000020, 0x0000000000002020,
0x0000000020000000, 0x0000000020002000, 0x0000000020000020, 0x0000000020002020,
0x0000000000200000, 0x0000000000202000, 0x0000000000200020, 0x0000000000202020,
0x0000000020200000, 0x0000000020202000, 0x0000000020200020, 0x0000000020202020, ),
( 0x0000000000000000, 0x0000200000000000, 0x0000002000000000, 0x0000202000000000,
0x2000000000000000, 0x2000200000000000, 0x2000002000000000, 0x2000202000000000,
0x0020000000000000, 0x0020200000000000, 0x0020002000000000, 0x0020202000000000,
0x2020000000000000, 0x2020200000000000, 0x2020002000000000, 0x2020202000000000, ),
( 0x0000000000000000, 0x0000000000004004, 0x0400000000000040, 0x0400000000004044,
0x0000000040040000, 0x0000000040044004, 0x0400000040040040, 0x0400000040044044,
0x0000000000400400, 0x0000000000404404, 0x0400000000400440, 0x0400000000404444,
0x0000000040440400, 0x0000000040444404, 0x0400000040440440, 0x0400000040444444, ),
( 0x0000000000000000, 0x0000400400000000, 0x0000004004000000, 0x0000404404000000,
0x4004000000000000, 0x4004400400000000, 0x4004004004000000, 0x4004404404000000,
0x0040040000000000, 0x0040440400000000, 0x0040044004000000, 0x0040444404000000,
0x4044040000000000, 0x4044440400000000, 0x4044044004000000, 0x4044444404000000, ),
)
#---------------------------------------------------------------
# Table that combines the S, P, and E operations.
#---------------------------------------------------------------
SPE=(
( 0x0080088008200000, 0x0000008008000000, 0x0000000000200020, 0x0080088008200020,
0x0000000000200000, 0x0080088008000020, 0x0000008008000020, 0x0000000000200020,
0x0080088008000020, 0x0080088008200000, 0x0000008008200000, 0x0080080000000020,
0x0080080000200020, 0x0000000000200000, 0x0000000000000000, 0x0000008008000020,
0x0000008008000000, 0x0000000000000020, 0x0080080000200000, 0x0080088008000000,
0x0080088008200020, 0x0000008008200000, 0x0080080000000020, 0x0080080000200000,
0x0000000000000020, 0x0080080000000000, 0x0080088008000000, 0x0000008008200020,
0x0080080000000000, 0x0080080000200020, 0x0000008008200020, 0x0000000000000000,
0x0000000000000000, 0x0080088008200020, 0x0080080000200000, 0x0000008008000020,
0x0080088008200000, 0x0000008008000000, 0x0080080000000020, 0x0080080000200000,
0x0000008008200020, 0x0080080000000000, 0x0080088008000000, 0x0000000000200020,
0x0080088008000020, 0x0000000000000020, 0x0000000000200020, 0x0000008008200000,
0x0080088008200020, 0x0080088008000000, 0x0000008008200000, 0x0080080000200020,
0x0000000000200000, 0x0080080000000020, 0x0000008008000020, 0x0000000000000000,
0x0000008008000000, 0x0000000000200000, 0x0080080000200020, 0x0080088008200000,
0x0000000000000020, 0x0000008008200020, 0x0080080000000000, 0x0080088008000020, ),
( 0x1000800810004004, 0x0000000000000000, 0x0000800810000000, 0x0000000010004004,
0x1000000000004004, 0x1000800800000000, 0x0000800800004004, 0x0000800810000000,
0x0000800800000000, 0x1000000010004004, 0x1000000000000000, 0x0000800800004004,
0x1000000010000000, 0x0000800810004004, 0x0000000010004004, 0x1000000000000000,
0x0000000010000000, 0x1000800800004004, 0x1000000010004004, 0x0000800800000000,
0x1000800810000000, 0x0000000000004004, 0x0000000000000000, 0x1000000010000000,
0x1000800800004004, 0x1000800810000000, 0x0000800810004004, 0x1000000000004004,
0x0000000000004004, 0x0000000010000000, 0x1000800800000000, 0x1000800810004004,
0x1000000010000000, 0x0000800810004004, 0x0000800800004004, 0x1000800810000000,
0x1000800810004004, 0x1000000010000000, 0x1000000000004004, 0x0000000000000000,
0x0000000000004004, 0x1000800800000000, 0x0000000010000000, 0x1000000010004004,
0x0000800800000000, 0x0000000000004004, 0x1000800810000000, 0x1000800800004004,
0x0000800810004004, 0x0000800800000000, 0x0000000000000000, 0x1000000000004004,
0x1000000000000000, 0x1000800810004004, 0x0000800810000000, 0x0000000010004004,
0x1000000010004004, 0x0000000010000000, 0x1000800800000000, 0x0000800800004004,
0x1000800800004004, 0x1000000000000000, 0x0000000010004004, 0x0000800810000000, ),
( 0x0000000000400410, 0x0010004004400400, 0x0010000000000000, 0x0010000000400410,
0x0000004004000010, 0x0000000000400400, 0x0010000000400410, 0x0010004004000000,
0x0010000000400400, 0x0000004004000000, 0x0000004004400400, 0x0000000000000010,
0x0010004004400410, 0x0010000000000010, 0x0000000000000010, 0x0000004004400410,
0x0000000000000000, 0x0000004004000010, 0x0010004004400400, 0x0010000000000000,
0x0010000000000010, 0x0010004004400410, 0x0000004004000000, 0x0000000000400410,
0x0000004004400410, 0x0010000000400400, 0x0010004004000010, 0x0000004004400400,
0x0010004004000000, 0x0000000000000000, 0x0000000000400400, 0x0010004004000010,
0x0010004004400400, 0x0010000000000000, 0x0000000000000010, 0x0000004004000000,
0x0010000000000010, 0x0000004004000010, 0x0000004004400400, 0x0010000000400410,
0x0000000000000000, 0x0010004004400400, 0x0010004004000000, 0x0000004004400410,
0x0000004004000010, 0x0000000000400400, 0x0010004004400410, 0x0000000000000010,
0x0010004004000010, 0x0000000000400410, 0x0000000000400400, 0x0010004004400410,
0x0000004004000000, 0x0010000000400400, 0x0010000000400410, 0x0010004004000000,
0x0010000000400400, 0x0000000000000000, 0x0000004004400410, 0x0010000000000010,
0x0000000000400410, 0x0010004004000010, 0x0010000000000000, 0x0000004004400400, ),
( 0x0800100040040080, 0x0000100000001000, 0x0800000000000080, 0x0800100040041080,
0x0000000000000000, 0x0000000040041000, 0x0800100000001080, 0x0800000040040080,
0x0000100040041000, 0x0800000000001080, 0x0000000000001000, 0x0800100000000080,
0x0800000000001080, 0x0800100040040080, 0x0000000040040000, 0x0000000000001000,
0x0800000040041080, 0x0000100040040000, 0x0000100000000000, 0x0800000000000080,
0x0000100040040000, 0x0800100000001080, 0x0000000040041000, 0x0000100000000000,
0x0800100000000080, 0x0000000000000000, 0x0800000040040080, 0x0000100040041000,
0x0000100000001000, 0x0800000040041080, 0x0800100040041080, 0x0000000040040000,
0x0800000040041080, 0x0800100000000080, 0x0000000040040000, 0x0800000000001080,
0x0000100040040000, 0x0000100000001000, 0x0800000000000080, 0x0000000040041000,
0x0800100000001080, 0x0000000000000000, 0x0000100000000000, 0x0800000040040080,
0x0000000000000000, 0x0800000040041080, 0x0000100040041000, 0x0000100000000000,
0x0000000000001000, 0x0800100040041080, 0x0800100040040080, 0x0000000040040000,
0x0800100040041080, 0x0800000000000080, 0x0000100000001000, 0x0800100040040080,
0x0800000040040080, 0x0000100040040000, 0x0000000040041000, 0x0800100000001080,
0x0800100000000080, 0x0000000000001000, 0x0800000000001080, 0x0000100040041000, ),
( 0x0000000000800800, 0x0000001000000000, 0x0040040000000000, 0x2040041000800800,
0x2000001000800800, 0x0040040000800800, 0x2040041000000000, 0x0000001000800800,
0x0000001000000000, 0x2000000000000000, 0x2000000000800800, 0x0040041000000000,
0x2040040000800800, 0x2000001000800800, 0x0040041000800800, 0x0000000000000000,
0x0040041000000000, 0x0000000000800800, 0x2000001000000000, 0x2040040000000000,
0x0040040000800800, 0x2040041000000000, 0x0000000000000000, 0x2000000000800800,
0x2000000000000000, 0x2040040000800800, 0x2040041000800800, 0x2000001000000000,
0x0000001000800800, 0x0040040000000000, 0x2040040000000000, 0x0040041000800800,
0x0040041000800800, 0x2040040000800800, 0x2000001000000000, 0x0000001000800800,
0x0000001000000000, 0x2000000000000000, 0x2000000000800800, 0x0040040000800800,
0x0000000000800800, 0x0040041000000000, 0x2040041000800800, 0x0000000000000000,
0x2040041000000000, 0x0000000000800800, 0x0040040000000000, 0x2000001000000000,
0x2040040000800800, 0x0040040000000000, 0x0000000000000000, 0x2040041000800800,
0x2000001000800800, 0x0040041000800800, 0x2040040000000000, 0x0000001000000000,
0x0040041000000000, 0x2000001000800800, 0x0040040000800800, 0x2040040000000000,
0x2000000000000000, 0x2040041000000000, 0x0000001000800800, 0x2000000000800800, ),
( 0x4004000000008008, 0x4004000020000000, 0x0000000000000000, 0x0000200020008008,
0x4004000020000000, 0x0000200000000000, 0x4004200000008008, 0x0000000020000000,
0x4004200000000000, 0x4004200020008008, 0x0000200020000000, 0x0000000000008008,
0x0000200000008008, 0x4004000000008008, 0x0000000020008008, 0x4004200020000000,
0x0000000020000000, 0x4004200000008008, 0x4004000020008008, 0x0000000000000000,
0x0000200000000000, 0x4004000000000000, 0x0000200020008008, 0x4004000020008008,
0x4004200020008008, 0x0000000020008008, 0x0000000000008008, 0x4004200000000000,
0x4004000000000000, 0x0000200020000000, 0x4004200020000000, 0x0000200000008008,
0x4004200000000000, 0x0000000000008008, 0x0000200000008008, 0x4004200020000000,
0x0000200020008008, 0x4004000020000000, 0x0000000000000000, 0x0000200000008008,
0x0000000000008008, 0x0000200000000000, 0x4004000020008008, 0x0000000020000000,
0x4004000020000000, 0x4004200020008008, 0x0000200020000000, 0x4004000000000000,
0x4004200020008008, 0x0000200020000000, 0x0000000020000000, 0x4004200000008008,
0x4004000000008008, 0x0000000020008008, 0x4004200020000000, 0x0000000000000000,
0x0000200000000000, 0x4004000000008008, 0x4004200000008008, 0x0000200020008008,
0x0000000020008008, 0x4004200000000000, 0x4004000000000000, 0x4004000020008008, ),
( 0x0000400400000000, 0x0020000000000000, 0x0020000000100000, 0x0400000000100040,
0x0420400400100040, 0x0400400400000040, 0x0020400400000000, 0x0000000000000000,
0x0000000000100000, 0x0420000000100040, 0x0420000000000040, 0x0000400400100000,
0x0400000000000040, 0x0020400400100000, 0x0000400400100000, 0x0420000000000040,
0x0420000000100040, 0x0000400400000000, 0x0400400400000040, 0x0420400400100040,
0x0000000000000000, 0x0020000000100000, 0x0400000000100040, 0x0020400400000000,
0x0400400400100040, 0x0420400400000040, 0x0020400400100000, 0x0400000000000040,
0x0420400400000040, 0x0400400400100040, 0x0020000000000000, 0x0000000000100000,
0x0420400400000040, 0x0000400400100000, 0x0400400400100040, 0x0420000000000040,
0x0000400400000000, 0x0020000000000000, 0x0000000000100000, 0x0400400400100040,
0x0420000000100040, 0x0420400400000040, 0x0020400400000000, 0x0000000000000000,
0x0020000000000000, 0x0400000000100040, 0x0400000000000040, 0x0020000000100000,
0x0000000000000000, 0x0420000000100040, 0x0020000000100000, 0x0020400400000000,
0x0420000000000040, 0x0000400400000000, 0x0420400400100040, 0x0000000000100000,
0x0020400400100000, 0x0400000000000040, 0x0400400400000040, 0x0420400400100040,
0x0400000000100040, 0x0020400400100000, 0x0000400400100000, 0x0400400400000040, ),
( 0x8008000080082000, 0x0000002080082000, 0x8008002000000000, 0x0000000000000000,
0x0000002000002000, 0x8008000080080000, 0x0000000080082000, 0x8008002080082000,
0x8008000000000000, 0x0000000000002000, 0x0000002080080000, 0x8008002000000000,
0x8008002080080000, 0x8008002000002000, 0x8008000000002000, 0x0000000080082000,
0x0000002000000000, 0x8008002080080000, 0x8008000080080000, 0x0000002000002000,
0x8008002080082000, 0x8008000000002000, 0x0000000000000000, 0x0000002080080000,
0x0000000000002000, 0x0000000080080000, 0x8008002000002000, 0x8008000080082000,
0x0000000080080000, 0x0000002000000000, 0x0000002080082000, 0x8008000000000000,
0x0000000080080000, 0x0000002000000000, 0x8008000000002000, 0x8008002080082000,
0x8008002000000000, 0x0000000000002000, 0x0000000000000000, 0x0000002080080000,
0x8008000080082000, 0x8008002000002000, 0x0000002000002000, 0x8008000080080000,
0x0000002080082000, 0x8008000000000000, 0x8008000080080000, 0x0000002000002000,
0x8008002080082000, 0x0000000080080000, 0x0000000080082000, 0x8008000000002000,
0x0000002080080000, 0x8008002000000000, 0x8008002000002000, 0x0000000080082000,
0x8008000000000000, 0x0000002080082000, 0x8008002080080000, 0x0000000000000000,
0x0000000000002000, 0x8008000080082000, 0x0000002000000000, 0x8008002080080000, ),
)
#---------------------------------------------------------------
# compressed/interleaved => final permutation table
# Compression, final permutation, bit reverse
#---------------------------------------------------------------
# NOTE: this was reordered from original table to make perm6464 logic simpler
CF6464=(
( 0x0000000000000000, 0x0000002000000000, 0x0000200000000000, 0x0000202000000000,
0x0020000000000000, 0x0020002000000000, 0x0020200000000000, 0x0020202000000000,
0x2000000000000000, 0x2000002000000000, 0x2000200000000000, 0x2000202000000000,
0x2020000000000000, 0x2020002000000000, 0x2020200000000000, 0x2020202000000000, ),
( 0x0000000000000000, 0x0000000200000000, 0x0000020000000000, 0x0000020200000000,
0x0002000000000000, 0x0002000200000000, 0x0002020000000000, 0x0002020200000000,
0x0200000000000000, 0x0200000200000000, 0x0200020000000000, 0x0200020200000000,
0x0202000000000000, 0x0202000200000000, 0x0202020000000000, 0x0202020200000000, ),
( 0x0000000000000000, 0x0000000000000020, 0x0000000000002000, 0x0000000000002020,
0x0000000000200000, 0x0000000000200020, 0x0000000000202000, 0x0000000000202020,
0x0000000020000000, 0x0000000020000020, 0x0000000020002000, 0x0000000020002020,
0x0000000020200000, 0x0000000020200020, 0x0000000020202000, 0x0000000020202020, ),
( 0x0000000000000000, 0x0000000000000002, 0x0000000000000200, 0x0000000000000202,
0x0000000000020000, 0x0000000000020002, 0x0000000000020200, 0x0000000000020202,
0x0000000002000000, 0x0000000002000002, 0x0000000002000200, 0x0000000002000202,
0x0000000002020000, 0x0000000002020002, 0x0000000002020200, 0x0000000002020202, ),
( 0x0000000000000000, 0x0000008000000000, 0x0000800000000000, 0x0000808000000000,
0x0080000000000000, 0x0080008000000000, 0x0080800000000000, 0x0080808000000000,
0x8000000000000000, 0x8000008000000000, 0x8000800000000000, 0x8000808000000000,
0x8080000000000000, 0x8080008000000000, 0x8080800000000000, 0x8080808000000000, ),
( 0x0000000000000000, 0x0000000800000000, 0x0000080000000000, 0x0000080800000000,
0x0008000000000000, 0x0008000800000000, 0x0008080000000000, 0x0008080800000000,
0x0800000000000000, 0x0800000800000000, 0x0800080000000000, 0x0800080800000000,
0x0808000000000000, 0x0808000800000000, 0x0808080000000000, 0x0808080800000000, ),
( 0x0000000000000000, 0x0000000000000080, 0x0000000000008000, 0x0000000000008080,
0x0000000000800000, 0x0000000000800080, 0x0000000000808000, 0x0000000000808080,
0x0000000080000000, 0x0000000080000080, 0x0000000080008000, 0x0000000080008080,
0x0000000080800000, 0x0000000080800080, 0x0000000080808000, 0x0000000080808080, ),
( 0x0000000000000000, 0x0000000000000008, 0x0000000000000800, 0x0000000000000808,
0x0000000000080000, 0x0000000000080008, 0x0000000000080800, 0x0000000000080808,
0x0000000008000000, 0x0000000008000008, 0x0000000008000800, 0x0000000008000808,
0x0000000008080000, 0x0000000008080008, 0x0000000008080800, 0x0000000008080808, ),
( 0x0000000000000000, 0x0000001000000000, 0x0000100000000000, 0x0000101000000000,
0x0010000000000000, 0x0010001000000000, 0x0010100000000000, 0x0010101000000000,
0x1000000000000000, 0x1000001000000000, 0x1000100000000000, 0x1000101000000000,
0x1010000000000000, 0x1010001000000000, 0x1010100000000000, 0x1010101000000000, ),
( 0x0000000000000000, 0x0000000100000000, 0x0000010000000000, 0x0000010100000000,
0x0001000000000000, 0x0001000100000000, 0x0001010000000000, 0x0001010100000000,
0x0100000000000000, 0x0100000100000000, 0x0100010000000000, 0x0100010100000000,
0x0101000000000000, 0x0101000100000000, 0x0101010000000000, 0x0101010100000000, ),
( 0x0000000000000000, 0x0000000000000010, 0x0000000000001000, 0x0000000000001010,
0x0000000000100000, 0x0000000000100010, 0x0000000000101000, 0x0000000000101010,
0x0000000010000000, 0x0000000010000010, 0x0000000010001000, 0x0000000010001010,
0x0000000010100000, 0x0000000010100010, 0x0000000010101000, 0x0000000010101010, ),
( 0x0000000000000000, 0x0000000000000001, 0x0000000000000100, 0x0000000000000101,
0x0000000000010000, 0x0000000000010001, 0x0000000000010100, 0x0000000000010101,
0x0000000001000000, 0x0000000001000001, 0x0000000001000100, 0x0000000001000101,
0x0000000001010000, 0x0000000001010001, 0x0000000001010100, 0x0000000001010101, ),
( 0x0000000000000000, 0x0000004000000000, 0x0000400000000000, 0x0000404000000000,
0x0040000000000000, 0x0040004000000000, 0x0040400000000000, 0x0040404000000000,
0x4000000000000000, 0x4000004000000000, 0x4000400000000000, 0x4000404000000000,
0x4040000000000000, 0x4040004000000000, 0x4040400000000000, 0x4040404000000000, ),
( 0x0000000000000000, 0x0000000400000000, 0x0000040000000000, 0x0000040400000000,
0x0004000000000000, 0x0004000400000000, 0x0004040000000000, 0x0004040400000000,
0x0400000000000000, 0x0400000400000000, 0x0400040000000000, 0x0400040400000000,
0x0404000000000000, 0x0404000400000000, 0x0404040000000000, 0x0404040400000000, ),
( 0x0000000000000000, 0x0000000000000040, 0x0000000000004000, 0x0000000000004040,
0x0000000000400000, 0x0000000000400040, 0x0000000000404000, 0x0000000000404040,
0x0000000040000000, 0x0000000040000040, 0x0000000040004000, 0x0000000040004040,
0x0000000040400000, 0x0000000040400040, 0x0000000040404000, 0x0000000040404040, ),
( 0x0000000000000000, 0x0000000000000004, 0x0000000000000400, 0x0000000000000404,
0x0000000000040000, 0x0000000000040004, 0x0000000000040400, 0x0000000000040404,
0x0000000004000000, 0x0000000004000004, 0x0000000004000400, 0x0000000004000404,
0x0000000004040000, 0x0000000004040004, 0x0000000004040400, 0x0000000004040404, ),
)
#===================================================================
# eof _load_tables()
#===================================================================
#=============================================================================
# support
#=============================================================================
def _permute(c, p):
"""Returns the permutation of the given 32-bit or 64-bit code with
the specified permutation table."""
# NOTE: only difference between 32 & 64 bit permutations
# is that len(p)==8 for 32 bit, and len(p)==16 for 64 bit.
out = 0
for r in p:
out |= r[c&0xf]
c >>= 4
return out
#=============================================================================
# packing & unpacking
#=============================================================================
_uint64_struct = struct.Struct(">Q")
_BNULL = b('\x00')
def _pack64(value):
return _uint64_struct.pack(value)
def _unpack64(value):
return _uint64_struct.unpack(value)[0]
def _pack56(value):
return _uint64_struct.pack(value)[1:]
def _unpack56(value):
return _uint64_struct.unpack(_BNULL+value)[0]
#=============================================================================
# 56->64 key manipulation
#=============================================================================
##def expand_7bit(value):
## "expand 7-bit integer => 7-bits + 1 odd-parity bit"
## # parity calc adapted from 32-bit even parity alg found at
## # http://graphics.stanford.edu/~seander/bithacks.html#ParityParallel
## assert 0 <= value < 0x80, "value out of range"
## return (value<<1) | (0x9669 >> ((value ^ (value >> 4)) & 0xf)) & 1
_EXPAND_ITER = irange(49,-7,-7)
def expand_des_key(key):
"convert DES from 7 bytes to 8 bytes (by inserting empty parity bits)"
if isinstance(key, bytes):
if len(key) != 7:
raise ValueError("key must be 7 bytes in size")
elif isinstance(key, int_types):
if key < 0 or key > INT_56_MASK:
raise ValueError("key must be 56-bit non-negative integer")
return _unpack64(expand_des_key(_pack56(key)))
else:
raise exc.ExpectedTypeError(key, "bytes or int", "key")
key = _unpack56(key)
# NOTE: the following would insert correctly-valued parity bits in each key,
# but the parity bit would just be ignored in des_encrypt_block(),
# so not bothering to use it.
##return join_byte_values(expand_7bit((key >> shift) & 0x7f)
## for shift in _EXPAND_ITER)
return join_byte_values(((key>>shift) & 0x7f)<<1 for shift in _EXPAND_ITER)
def shrink_des_key(key):
"convert DES key from 8 bytes to 7 bytes (by discarding the parity bits)"
if isinstance(key, bytes):
if len(key) != 8:
raise ValueError("key must be 8 bytes in size")
return _pack56(shrink_des_key(_unpack64(key)))
elif isinstance(key, int_types):
if key < 0 or key > INT_64_MASK:
raise ValueError("key must be 64-bit non-negative integer")
else:
raise exc.ExpectedTypeError(key, "bytes or int", "key")
key >>= 1
result = 0
offset = 0
while offset < 56:
result |= (key & 0x7f)<<offset
key >>= 8
offset += 7
assert not (result & ~INT_64_MASK)
return result
#=============================================================================
# des encryption
#=============================================================================
def des_encrypt_block(key, input, salt=0, rounds=1):
"""encrypt single block of data using DES, operates on 8-byte strings.
:arg key:
DES key as 7 byte string, or 8 byte string with parity bits
(parity bit values are ignored).
:arg input:
plaintext block to encrypt, as 8 byte string.
:arg salt:
Optional 24-bit integer used to mutate the base DES algorithm in a
manner specific to :class:`~passlib.hash.des_crypt` and it's variants.
The default value ``0`` provides the normal (unsalted) DES behavior.
The salt functions as follows:
if the ``i``'th bit of ``salt`` is set,
bits ``i`` and ``i+24`` are swapped in the DES E-box output.
:arg rounds:
Optional number of rounds of to apply the DES key schedule.
the default (``rounds=1``) provides the normal DES behavior,
but :class:`~passlib.hash.des_crypt` and it's variants use
alternate rounds values.
:raises TypeError: if any of the provided args are of the wrong type.
:raises ValueError:
if any of the input blocks are the wrong size,
or the salt/rounds values are out of range.
:returns:
resulting 8-byte ciphertext block.
"""
# validate & unpack key
if isinstance(key, bytes):
if len(key) == 7:
key = expand_des_key(key)
elif len(key) != 8:
raise ValueError("key must be 7 or 8 bytes")
key = _unpack64(key)
else:
raise exc.ExpectedTypeError(key, "bytes", "key")
# validate & unpack input
if isinstance(input, bytes):
if len(input) != 8:
raise ValueError("input block must be 8 bytes")
input = _unpack64(input)
else:
raise exc.ExpectedTypeError(input, "bytes", "input")
# hand things off to other func
result = des_encrypt_int_block(key, input, salt, rounds)
# repack result
return _pack64(result)
def des_encrypt_int_block(key, input, salt=0, rounds=1):
"""encrypt single block of data using DES, operates on 64-bit integers.
this function is essentially the same as :func:`des_encrypt_block`,
except that it operates on integers, and will NOT automatically
expand 56-bit keys if provided (since there's no way to detect them).
:arg key:
DES key as 64-bit integer (the parity bits are ignored).
:arg input:
input block as 64-bit integer
:arg salt:
optional 24-bit integer used to mutate the base DES algorithm.
defaults to ``0`` (no mutation applied).
:arg rounds:
optional number of rounds of to apply the DES key schedule.
defaults to ``1``.
:raises TypeError: if any of the provided args are of the wrong type.
:raises ValueError:
if any of the input blocks are the wrong size,
or the salt/rounds values are out of range.
:returns:
resulting ciphertext as 64-bit integer.
"""
#---------------------------------------------------------------
# input validation
#---------------------------------------------------------------
# validate salt, rounds
if rounds < 1:
raise ValueError("rounds must be positive integer")
if salt < 0 or salt > INT_24_MASK:
raise ValueError("salt must be 24-bit non-negative integer")
# validate & unpack key
if not isinstance(key, int_types):
raise exc.ExpectedTypeError(key, "int", "key")
elif key < 0 or key > INT_64_MASK:
raise ValueError("key must be 64-bit non-negative integer")
# validate & unpack input
if not isinstance(input, int_types):
raise exc.ExpectedTypeError(input, "int", "input")
elif input < 0 or input > INT_64_MASK:
raise ValueError("input must be 64-bit non-negative integer")
#---------------------------------------------------------------
# DES setup
#---------------------------------------------------------------
# load tables if not already done
global SPE, PCXROT, IE3264, CF6464
if PCXROT is None:
_load_tables()
# load SPE into local vars to speed things up and remove an array access call
SPE0, SPE1, SPE2, SPE3, SPE4, SPE5, SPE6, SPE7 = SPE
# NOTE: parity bits are ignored completely
# (UTs do fuzz testing to ensure this)
# generate key schedule
# NOTE: generation was modified to output two elements at a time,
# so that per-round loop could do two passes at once.
def _iter_key_schedule(ks_odd):
"given 64-bit key, iterates over the 8 (even,odd) key schedule pairs"
for p_even, p_odd in PCXROT:
ks_even = _permute(ks_odd, p_even)
ks_odd = _permute(ks_even, p_odd)
yield ks_even & _KS_MASK, ks_odd & _KS_MASK
ks_list = list(_iter_key_schedule(key))
# expand 24 bit salt -> 32 bit per des_crypt & bsdi_crypt
salt = (
((salt & 0x00003f) << 26) |
((salt & 0x000fc0) << 12) |
((salt & 0x03f000) >> 2) |
((salt & 0xfc0000) >> 16)
)
# init L & R
if input == 0:
L = R = 0
else:
L = ((input >> 31) & 0xaaaaaaaa) | (input & 0x55555555)
L = _permute(L, IE3264)
R = ((input >> 32) & 0xaaaaaaaa) | ((input >> 1) & 0x55555555)
R = _permute(R, IE3264)
#---------------------------------------------------------------
# main DES loop - run for specified number of rounds
#---------------------------------------------------------------
while rounds:
rounds -= 1
# run over each part of the schedule, 2 parts at a time
for ks_even, ks_odd in ks_list:
k = ((R>>32) ^ R) & salt # use the salt to flip specific bits
B = (k<<32) ^ k ^ R ^ ks_even
L ^= (SPE0[(B>>58)&0x3f] ^ SPE1[(B>>50)&0x3f] ^
SPE2[(B>>42)&0x3f] ^ SPE3[(B>>34)&0x3f] ^
SPE4[(B>>26)&0x3f] ^ SPE5[(B>>18)&0x3f] ^
SPE6[(B>>10)&0x3f] ^ SPE7[(B>>2)&0x3f])
k = ((L>>32) ^ L) & salt # use the salt to flip specific bits
B = (k<<32) ^ k ^ L ^ ks_odd
R ^= (SPE0[(B>>58)&0x3f] ^ SPE1[(B>>50)&0x3f] ^
SPE2[(B>>42)&0x3f] ^ SPE3[(B>>34)&0x3f] ^
SPE4[(B>>26)&0x3f] ^ SPE5[(B>>18)&0x3f] ^
SPE6[(B>>10)&0x3f] ^ SPE7[(B>>2)&0x3f])
# swap L and R
L, R = R, L
#---------------------------------------------------------------
# return final result
#---------------------------------------------------------------
C = (
((L>>3) & 0x0f0f0f0f00000000)
|
((L<<33) & 0xf0f0f0f000000000)
|
((R>>35) & 0x000000000f0f0f0f)
|
((R<<1) & 0x00000000f0f0f0f0)
)
return _permute(C, CF6464)
@deprecated_function(deprecated="1.6", removed="1.8",
replacement="des_encrypt_int_block()")
def mdes_encrypt_int_block(key, input, salt=0, rounds=1): # pragma: no cover -- deprecated & unused
if isinstance(key, bytes):
if len(key) == 7:
key = expand_des_key(key)
key = _unpack64(key)
return des_encrypt_int_block(key, input, salt, rounds)
#=============================================================================
# eof
#=============================================================================

1664
passlib/utils/handlers.py Normal file

File diff suppressed because it is too large Load Diff

266
passlib/utils/md4.py Normal file
View File

@ -0,0 +1,266 @@
"""
helper implementing insecure and obsolete md4 algorithm.
used for NTHASH format, which is also insecure and broken,
since it's just md4(password)
implementated based on rfc at http://www.faqs.org/rfcs/rfc1320.html
"""
#=============================================================================
# imports
#=============================================================================
# core
from binascii import hexlify
import struct
from warnings import warn
# site
from passlib.utils.compat import b, bytes, bascii_to_str, irange, PY3
# local
__all__ = [ "md4" ]
#=============================================================================
# utils
#=============================================================================
def F(x,y,z):
return (x&y) | ((~x) & z)
def G(x,y,z):
return (x&y) | (x&z) | (y&z)
##def H(x,y,z):
## return x ^ y ^ z
MASK_32 = 2**32-1
#=============================================================================
# main class
#=============================================================================
class md4(object):
"""pep-247 compatible implementation of MD4 hash algorithm
.. attribute:: digest_size
size of md4 digest in bytes (16 bytes)
.. method:: update
update digest by appending additional content
.. method:: copy
create clone of digest object, including current state
.. method:: digest
return bytes representing md4 digest of current content
.. method:: hexdigest
return hexdecimal version of digest
"""
# FIXME: make this follow hash object PEP better.
# FIXME: this isn't threadsafe
# XXX: should we monkeypatch ourselves into hashlib for general use? probably wouldn't be nice.
name = "md4"
digest_size = digestsize = 16
_count = 0 # number of 64-byte blocks processed so far (not including _buf)
_state = None # list of [a,b,c,d] 32 bit ints used as internal register
_buf = None # data processed in 64 byte blocks, this holds leftover from last update
def __init__(self, content=None):
self._count = 0
self._state = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476]
self._buf = b('')
if content:
self.update(content)
# round 1 table - [abcd k s]
_round1 = [
[0,1,2,3, 0,3],
[3,0,1,2, 1,7],
[2,3,0,1, 2,11],
[1,2,3,0, 3,19],
[0,1,2,3, 4,3],
[3,0,1,2, 5,7],
[2,3,0,1, 6,11],
[1,2,3,0, 7,19],
[0,1,2,3, 8,3],
[3,0,1,2, 9,7],
[2,3,0,1, 10,11],
[1,2,3,0, 11,19],
[0,1,2,3, 12,3],
[3,0,1,2, 13,7],
[2,3,0,1, 14,11],
[1,2,3,0, 15,19],
]
# round 2 table - [abcd k s]
_round2 = [
[0,1,2,3, 0,3],
[3,0,1,2, 4,5],
[2,3,0,1, 8,9],
[1,2,3,0, 12,13],
[0,1,2,3, 1,3],
[3,0,1,2, 5,5],
[2,3,0,1, 9,9],
[1,2,3,0, 13,13],
[0,1,2,3, 2,3],
[3,0,1,2, 6,5],
[2,3,0,1, 10,9],
[1,2,3,0, 14,13],
[0,1,2,3, 3,3],
[3,0,1,2, 7,5],
[2,3,0,1, 11,9],
[1,2,3,0, 15,13],
]
# round 3 table - [abcd k s]
_round3 = [
[0,1,2,3, 0,3],
[3,0,1,2, 8,9],
[2,3,0,1, 4,11],
[1,2,3,0, 12,15],
[0,1,2,3, 2,3],
[3,0,1,2, 10,9],
[2,3,0,1, 6,11],
[1,2,3,0, 14,15],
[0,1,2,3, 1,3],
[3,0,1,2, 9,9],
[2,3,0,1, 5,11],
[1,2,3,0, 13,15],
[0,1,2,3, 3,3],
[3,0,1,2, 11,9],
[2,3,0,1, 7,11],
[1,2,3,0, 15,15],
]
def _process(self, block):
"process 64 byte block"
# unpack block into 16 32-bit ints
X = struct.unpack("<16I", block)
# clone state
orig = self._state
state = list(orig)
# round 1 - F function - (x&y)|(~x & z)
for a,b,c,d,k,s in self._round1:
t = (state[a] + F(state[b],state[c],state[d]) + X[k]) & MASK_32
state[a] = ((t<<s) & MASK_32) + (t>>(32-s))
# round 2 - G function
for a,b,c,d,k,s in self._round2:
t = (state[a] + G(state[b],state[c],state[d]) + X[k] + 0x5a827999) & MASK_32
state[a] = ((t<<s) & MASK_32) + (t>>(32-s))
# round 3 - H function - x ^ y ^ z
for a,b,c,d,k,s in self._round3:
t = (state[a] + (state[b] ^ state[c] ^ state[d]) + X[k] + 0x6ed9eba1) & MASK_32
state[a] = ((t<<s) & MASK_32) + (t>>(32-s))
# add back into original state
for i in irange(4):
orig[i] = (orig[i]+state[i]) & MASK_32
def update(self, content):
if not isinstance(content, bytes):
raise TypeError("expected bytes")
buf = self._buf
if buf:
content = buf + content
idx = 0
end = len(content)
while True:
next = idx + 64
if next <= end:
self._process(content[idx:next])
self._count += 1
idx = next
else:
self._buf = content[idx:]
return
def copy(self):
other = _builtin_md4()
other._count = self._count
other._state = list(self._state)
other._buf = self._buf
return other
def digest(self):
# NOTE: backing up state so we can restore it after _process is called,
# in case object is updated again (this is only attr altered by this method)
orig = list(self._state)
# final block: buf + 0x80,
# then 0x00 padding until congruent w/ 56 mod 64 bytes
# then last 8 bytes = msg length in bits
buf = self._buf
msglen = self._count*512 + len(buf)*8
block = buf + b('\x80') + b('\x00') * ((119-len(buf)) % 64) + \
struct.pack("<2I", msglen & MASK_32, (msglen>>32) & MASK_32)
if len(block) == 128:
self._process(block[:64])
self._process(block[64:])
else:
assert len(block) == 64
self._process(block)
# render digest & restore un-finalized state
out = struct.pack("<4I", *self._state)
self._state = orig
return out
def hexdigest(self):
return bascii_to_str(hexlify(self.digest()))
#===================================================================
# eoc
#===================================================================
# keep ref around for unittest, 'md4' usually replaced by ssl wrapper, below.
_builtin_md4 = md4
#=============================================================================
# check if hashlib provides accelarated md4
#=============================================================================
import hashlib
from passlib.utils import PYPY
def _has_native_md4(): # pragma: no cover -- runtime detection
try:
h = hashlib.new("md4")
except ValueError:
# not supported - ssl probably missing (e.g. ironpython)
return False
result = h.hexdigest()
if result == '31d6cfe0d16ae931b73c59d7e0c089c0':
return True
if PYPY and result == '':
# workaround for https://bugs.pypy.org/issue957, fixed in PyPy 1.8
return False
# anything else and we should alert user
from passlib.exc import PasslibRuntimeWarning
warn("native md4 support disabled, sanity check failed!", PasslibRuntimeWarning)
return False
if _has_native_md4():
# overwrite md4 class w/ hashlib wrapper
def md4(content=None):
"wrapper for hashlib.new('md4')"
return hashlib.new('md4', content or b(''))
#=============================================================================
# eof
#=============================================================================

415
passlib/utils/pbkdf2.py Normal file
View File

@ -0,0 +1,415 @@
"""passlib.pbkdf2 - PBKDF2 support
this module is getting increasingly poorly named.
maybe rename to "kdf" since it's getting more key derivation functions added.
"""
#=============================================================================
# imports
#=============================================================================
# core
import hashlib
import logging; log = logging.getLogger(__name__)
import re
from struct import pack
from warnings import warn
# site
try:
from M2Crypto import EVP as _EVP
except ImportError:
_EVP = None
# pkg
from passlib.exc import PasslibRuntimeWarning, ExpectedTypeError
from passlib.utils import join_bytes, to_native_str, bytes_to_int, int_to_bytes, join_byte_values
from passlib.utils.compat import b, bytes, BytesIO, irange, callable, int_types
# local
__all__ = [
"get_prf",
"pbkdf1",
"pbkdf2",
]
#=============================================================================
# hash helpers
#=============================================================================
# known hash names
_nhn_formats = dict(hashlib=0, iana=1)
_nhn_hash_names = [
# (hashlib/ssl name, iana name or standin, ... other known aliases)
# hashes with official IANA-assigned names
# (as of 2012-03 - http://www.iana.org/assignments/hash-function-text-names)
("md2", "md2"),
("md5", "md5"),
("sha1", "sha-1"),
("sha224", "sha-224", "sha2-224"),
("sha256", "sha-256", "sha2-256"),
("sha384", "sha-384", "sha2-384"),
("sha512", "sha-512", "sha2-512"),
# hashlib/ssl-supported hashes without official IANA names,
# hopefully compatible stand-ins have been chosen.
("md4", "md4"),
("sha", "sha-0", "sha0"),
("ripemd", "ripemd"),
("ripemd160", "ripemd-160"),
]
# cache for norm_hash_name()
_nhn_cache = {}
def norm_hash_name(name, format="hashlib"):
"""Normalize hash function name
:arg name:
Original hash function name.
This name can be a Python :mod:`~hashlib` digest name,
a SCRAM mechanism name, IANA assigned hash name, etc.
Case is ignored, and underscores are converted to hyphens.
:param format:
Naming convention to normalize to.
Possible values are:
* ``"hashlib"`` (the default) - normalizes name to be compatible
with Python's :mod:`!hashlib`.
* ``"iana"`` - normalizes name to IANA-assigned hash function name.
for hashes which IANA hasn't assigned a name for, issues a warning,
and then uses a heuristic to give a "best guess".
:returns:
Hash name, returned as native :class:`!str`.
"""
# check cache
try:
idx = _nhn_formats[format]
except KeyError:
raise ValueError("unknown format: %r" % (format,))
try:
return _nhn_cache[name][idx]
except KeyError:
pass
orig = name
# normalize input
if not isinstance(name, str):
name = to_native_str(name, 'utf-8', 'hash name')
name = re.sub("[_ /]", "-", name.strip().lower())
if name.startswith("scram-"):
name = name[6:]
if name.endswith("-plus"):
name = name[:-5]
# look through standard names and known aliases
def check_table(name):
for row in _nhn_hash_names:
if name in row:
_nhn_cache[orig] = row
return row[idx]
result = check_table(name)
if result:
return result
# try to clean name up, and recheck table
m = re.match("^(?P<name>[a-z]+)-?(?P<rev>\d)?-?(?P<size>\d{3,4})?$", name)
if m:
name, rev, size = m.group("name", "rev", "size")
if rev:
name += rev
if size:
name += "-" + size
result = check_table(name)
if result:
return result
# else we've done what we can
warn("norm_hash_name(): unknown hash: %r" % (orig,), PasslibRuntimeWarning)
name2 = name.replace("-", "")
row = _nhn_cache[orig] = (name2, name)
return row[idx]
# TODO: get_hash() func which wraps norm_hash_name(), hashlib.<attr>, and hashlib.new
#=============================================================================
# general prf lookup
#=============================================================================
_BNULL = b('\x00')
_XY_DIGEST = b(',\x1cb\xe0H\xa5\x82M\xfb>\xd6\x98\xef\x8e\xf9oQ\x85\xa3i')
_trans_5C = join_byte_values((x ^ 0x5C) for x in irange(256))
_trans_36 = join_byte_values((x ^ 0x36) for x in irange(256))
def _get_hmac_prf(digest):
"helper to return HMAC prf for specific digest"
def tag_wrapper(prf):
prf.__name__ = "hmac_" + digest
prf.__doc__ = ("hmac_%s(key, msg) -> digest;"
" generated by passlib.utils.pbkdf2.get_prf()" %
digest)
if _EVP and digest == "sha1":
# use m2crypto function directly for sha1, since that's it's default digest
try:
result = _EVP.hmac(b('x'),b('y'))
except ValueError: # pragma: no cover
pass
else:
if result == _XY_DIGEST:
return _EVP.hmac, 20
# don't expect to ever get here, but will fall back to pure-python if we do.
warn("M2Crypto.EVP.HMAC() returned unexpected result " # pragma: no cover -- sanity check
"during Passlib self-test!", PasslibRuntimeWarning)
elif _EVP:
# use m2crypto if it's present and supports requested digest
try:
result = _EVP.hmac(b('x'), b('y'), digest)
except ValueError:
pass
else:
# it does. so use M2Crypto's hmac & digest code
hmac_const = _EVP.hmac
def prf(key, msg):
return hmac_const(key, msg, digest)
digest_size = len(result)
tag_wrapper(prf)
return prf, digest_size
# fall back to hashlib-based implementation
digest_const = getattr(hashlib, digest, None)
if not digest_const:
raise ValueError("unknown hash algorithm: %r" % (digest,))
tmp = digest_const()
block_size = tmp.block_size
assert block_size >= 16, "unacceptably low block size"
digest_size = tmp.digest_size
del tmp
def prf(key, msg):
# simplified version of stdlib's hmac module
if len(key) > block_size:
key = digest_const(key).digest()
key += _BNULL * (block_size - len(key))
tmp = digest_const(key.translate(_trans_36) + msg).digest()
return digest_const(key.translate(_trans_5C) + tmp).digest()
tag_wrapper(prf)
return prf, digest_size
# cache mapping prf name/func -> (func, digest_size)
_prf_cache = {}
def _clear_prf_cache():
"helper for unit tests"
_prf_cache.clear()
def get_prf(name):
"""lookup pseudo-random family (prf) by name.
:arg name:
this must be the name of a recognized prf.
currently this only recognizes names with the format
:samp:`hmac-{digest}`, where :samp:`{digest}`
is the name of a hash function such as
``md5``, ``sha256``, etc.
this can also be a callable with the signature
``prf(secret, message) -> digest``,
in which case it will be returned unchanged.
:raises ValueError: if the name is not known
:raises TypeError: if the name is not a callable or string
:returns:
a tuple of :samp:`({func}, {digest_size})`.
* :samp:`{func}` is a function implementing
the specified prf, and has the signature
``func(secret, message) -> digest``.
* :samp:`{digest_size}` is an integer indicating
the number of bytes the function returns.
usage example::
>>> from passlib.utils.pbkdf2 import get_prf
>>> hmac_sha256, dsize = get_prf("hmac-sha256")
>>> hmac_sha256
<function hmac_sha256 at 0x1e37c80>
>>> dsize
32
>>> digest = hmac_sha256('password', 'message')
this function will attempt to return the fastest implementation
it can find; if M2Crypto is present, and supports the specified prf,
:func:`M2Crypto.EVP.hmac` will be used behind the scenes.
"""
global _prf_cache
if name in _prf_cache:
return _prf_cache[name]
if isinstance(name, str):
if name.startswith("hmac-") or name.startswith("hmac_"):
retval = _get_hmac_prf(name[5:])
else:
raise ValueError("unknown prf algorithm: %r" % (name,))
elif callable(name):
# assume it's a callable, use it directly
digest_size = len(name(b('x'),b('y')))
retval = (name, digest_size)
else:
raise ExpectedTypeError(name, "str or callable", "prf name")
_prf_cache[name] = retval
return retval
#=============================================================================
# pbkdf1 support
#=============================================================================
def pbkdf1(secret, salt, rounds, keylen=None, hash="sha1"):
"""pkcs#5 password-based key derivation v1.5
:arg secret: passphrase to use to generate key
:arg salt: salt string to use when generating key
:param rounds: number of rounds to use to generate key
:arg keylen: number of bytes to generate (if ``None``, uses digest's native size)
:param hash:
hash function to use. must be name of a hash recognized by hashlib.
:returns:
raw bytes of generated key
.. note::
This algorithm has been deprecated, new code should use PBKDF2.
Among other limitations, ``keylen`` cannot be larger
than the digest size of the specified hash.
"""
# validate secret & salt
if not isinstance(secret, bytes):
raise ExpectedTypeError(secret, "bytes", "secret")
if not isinstance(salt, bytes):
raise ExpectedTypeError(salt, "bytes", "salt")
# validate rounds
if not isinstance(rounds, int_types):
raise ExpectedTypeError(rounds, "int", "rounds")
if rounds < 1:
raise ValueError("rounds must be at least 1")
# resolve hash
try:
hash_const = getattr(hashlib, hash)
except AttributeError:
# check for ssl hash
# NOTE: if hash unknown, new() will throw ValueError, which we'd just
# reraise anyways; so instead of checking, we just let it get
# thrown during first use, below
# TODO: use builtin md4 class if hashlib doesn't have it.
def hash_const(msg):
return hashlib.new(hash, msg)
# prime pbkdf1 loop, get block size
block = hash_const(secret + salt).digest()
# validate keylen
if keylen is None:
keylen = len(block)
elif not isinstance(keylen, int_types):
raise ExpectedTypeError(keylen, "int or None", "keylen")
elif keylen < 0:
raise ValueError("keylen must be at least 0")
elif keylen > len(block):
raise ValueError("keylength too large for digest: %r > %r" %
(keylen, len(block)))
# main pbkdf1 loop
for _ in irange(rounds-1):
block = hash_const(block).digest()
return block[:keylen]
#=============================================================================
# pbkdf2
#=============================================================================
MAX_BLOCKS = 0xffffffff # 2**32-1
MAX_HMAC_SHA1_KEYLEN = MAX_BLOCKS*20
# NOTE: the pbkdf2 spec does not specify a maximum number of rounds.
# however, many of the hashes in passlib are currently clamped
# at the 32-bit limit, just for sanity. once realistic pbkdf2 rounds
# start approaching 24 bits, this limit will be raised.
def pbkdf2(secret, salt, rounds, keylen=None, prf="hmac-sha1"):
"""pkcs#5 password-based key derivation v2.0
:arg secret: passphrase to use to generate key
:arg salt: salt string to use when generating key
:param rounds: number of rounds to use to generate key
:arg keylen:
number of bytes to generate.
if set to ``None``, will use digest size of selected prf.
:param prf:
psuedo-random family to use for key strengthening.
this can be any string or callable accepted by :func:`get_prf`.
this defaults to ``"hmac-sha1"`` (the only prf explicitly listed in
the PBKDF2 specification)
:returns:
raw bytes of generated key
"""
# validate secret & salt
if not isinstance(secret, bytes):
raise ExpectedTypeError(secret, "bytes", "secret")
if not isinstance(salt, bytes):
raise ExpectedTypeError(salt, "bytes", "salt")
# validate rounds
if not isinstance(rounds, int_types):
raise ExpectedTypeError(rounds, "int", "rounds")
if rounds < 1:
raise ValueError("rounds must be at least 1")
# validate keylen
if keylen is not None:
if not isinstance(keylen, int_types):
raise ExpectedTypeError(keylen, "int or None", "keylen")
elif keylen < 0:
raise ValueError("keylen must be at least 0")
# special case for m2crypto + hmac-sha1
if prf == "hmac-sha1" and _EVP:
if keylen is None:
keylen = 20
# NOTE: doing check here, because M2crypto won't take 'long' instances
# (which this is when running under 32bit)
if keylen > MAX_HMAC_SHA1_KEYLEN:
raise ValueError("key length too long for digest")
# NOTE: as of 2012-4-4, m2crypto has buffer overflow issue
# which may cause segfaults if keylen > 32 (EVP_MAX_KEY_LENGTH).
# therefore we're avoiding m2crypto for large keys until that's fixed.
# see https://bugzilla.osafoundation.org/show_bug.cgi?id=13052
if keylen < 32:
return _EVP.pbkdf2(secret, salt, rounds, keylen)
# resolve prf
prf_func, digest_size = get_prf(prf)
if keylen is None:
keylen = digest_size
# figure out how many blocks we'll need
block_count = (keylen+digest_size-1)//digest_size
if block_count >= MAX_BLOCKS:
raise ValueError("key length too long for digest")
# build up result from blocks
def gen():
for i in irange(block_count):
digest = prf_func(secret, salt + pack(">L", i+1))
accum = bytes_to_int(digest)
for _ in irange(rounds-1):
digest = prf_func(secret, digest)
accum ^= bytes_to_int(digest)
yield int_to_bytes(accum, digest_size)
return join_bytes(gen())[:keylen]
#=============================================================================
# eof
#=============================================================================

68
passlib/win32.py Normal file
View File

@ -0,0 +1,68 @@
"""passlib.win32 - MS Windows support - DEPRECATED, WILL BE REMOVED IN 1.8
the LMHASH and NTHASH algorithms are used in various windows related contexts,
but generally not in a manner compatible with how passlib is structured.
in particular, they have no identifying marks, both being
32 bytes of binary data. thus, they can't be easily identified
in a context with other hashes, so a CryptHandler hasn't been defined for them.
this module provided two functions to aid in any use-cases which exist.
.. warning::
these functions should not be used for new code unless an existing
system requires them, they are both known broken,
and are beyond insecure on their own.
.. autofunction:: raw_lmhash
.. autofunction:: raw_nthash
See also :mod:`passlib.hash.nthash`.
"""
from warnings import warn
warn("the 'passlib.win32' module is deprecated, and will be removed in "
"passlib 1.8; please use the 'passlib.hash.nthash' and "
"'passlib.hash.lmhash' classes instead.",
DeprecationWarning)
#=============================================================================
# imports
#=============================================================================
# core
from binascii import hexlify
# site
# pkg
from passlib.utils.compat import b, unicode
from passlib.utils.des import des_encrypt_block
from passlib.hash import nthash
# local
__all__ = [
"nthash",
"raw_lmhash",
"raw_nthash",
]
#=============================================================================
# helpers
#=============================================================================
LM_MAGIC = b("KGS!@#$%")
raw_nthash = nthash.raw_nthash
def raw_lmhash(secret, encoding="ascii", hex=False):
"encode password using des-based LMHASH algorithm; returns string of raw bytes, or unicode hex"
# NOTE: various references say LMHASH uses the OEM codepage of the host
# for it's encoding. until a clear reference is found,
# as well as a path for getting the encoding,
# letting this default to "ascii" to prevent incorrect hashes
# from being made w/o user explicitly choosing an encoding.
if isinstance(secret, unicode):
secret = secret.encode(encoding)
ns = secret.upper()[:14] + b("\x00") * (14-len(secret))
out = des_encrypt_block(ns[:7], LM_MAGIC) + des_encrypt_block(ns[7:], LM_MAGIC)
return hexlify(out).decode("ascii") if hex else out
#=============================================================================
# eoc
#=============================================================================