mirror of
https://github.com/GAM-team/GAM.git
synced 2025-05-12 12:17:20 +00:00
yep
Merge branch 'master' of github.com:jay0lee/GAM
This commit is contained in:
commit
c52e646c67
20
gam.py
20
gam.py
@ -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
3
passlib/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
"""passlib - suite of password hashing & generation routinges"""
|
||||
|
||||
__version__ = '1.6.2'
|
1
passlib/_setup/__init__.py
Normal file
1
passlib/_setup/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""passlib.setup - helpers used by passlib's setup.py script"""
|
87
passlib/_setup/docdist.py
Normal file
87
passlib/_setup/docdist.py
Normal 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
57
passlib/_setup/stamp.py
Normal 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
1037
passlib/apache.py
Normal file
File diff suppressed because it is too large
Load Diff
192
passlib/apps.py
Normal file
192
passlib/apps.py
Normal 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
2711
passlib/context.py
Normal file
File diff suppressed because it is too large
Load Diff
184
passlib/exc.py
Normal file
184
passlib/exc.py
Normal 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
1
passlib/ext/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
|
6
passlib/ext/django/__init__.py
Normal file
6
passlib/ext/django/__init__.py
Normal 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.
|
||||
"""
|
323
passlib/ext/django/models.py
Normal file
323
passlib/ext/django/models.py
Normal 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
505
passlib/ext/django/utils.py
Normal 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
|
||||
#=============================================================================
|
1
passlib/handlers/__init__.py
Normal file
1
passlib/handlers/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""passlib.handlers -- holds implementations of all passlib's builtin hash formats"""
|
457
passlib/handlers/bcrypt.py
Normal file
457
passlib/handlers/bcrypt.py
Normal 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
219
passlib/handlers/cisco.py
Normal 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
|
||||
#=============================================================================
|
517
passlib/handlers/des_crypt.py
Normal file
517
passlib/handlers/des_crypt.py
Normal 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
144
passlib/handlers/digests.py
Normal 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
472
passlib/handlers/django.py
Normal 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
206
passlib/handlers/fshp.py
Normal 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
|
||||
#=============================================================================
|
270
passlib/handlers/ldap_digests.py
Normal file
270
passlib/handlers/ldap_digests.py
Normal 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
|
||||
#=============================================================================
|
333
passlib/handlers/md5_crypt.py
Normal file
333
passlib/handlers/md5_crypt.py
Normal 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
242
passlib/handlers/misc.py
Normal 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
246
passlib/handlers/mssql.py
Normal 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
128
passlib/handlers/mysql.py
Normal 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
175
passlib/handlers/oracle.py
Normal 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
490
passlib/handlers/pbkdf2.py
Normal 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
137
passlib/handlers/phpass.py
Normal 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
|
||||
#=============================================================================
|
57
passlib/handlers/postgres.py
Normal file
57
passlib/handlers/postgres.py
Normal 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
|
||||
#=============================================================================
|
29
passlib/handlers/roundup.py
Normal file
29
passlib/handlers/roundup.py
Normal file
@ -0,0 +1,29 @@
|
||||
"""passlib.handlers.roundup - Roundup issue tracker hashes"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
# core
|
||||
import logging; log = logging.getLogger(__name__)
|
||||
# site
|
||||
# pkg
|
||||
import passlib.utils.handlers as uh
|
||||
from passlib.utils.compat import u
|
||||
# local
|
||||
__all__ = [
|
||||
"roundup_plaintext",
|
||||
"ldap_hex_md5",
|
||||
"ldap_hex_sha1",
|
||||
]
|
||||
#=============================================================================
|
||||
#
|
||||
#=============================================================================
|
||||
roundup_plaintext = uh.PrefixWrapper("roundup_plaintext", "plaintext",
|
||||
prefix=u("{plaintext}"), lazy=True)
|
||||
|
||||
# NOTE: these are here because they're currently only known to be used by roundup
|
||||
ldap_hex_md5 = uh.PrefixWrapper("ldap_hex_md5", "hex_md5", u("{MD5}"), lazy=True)
|
||||
ldap_hex_sha1 = uh.PrefixWrapper("ldap_hex_sha1", "hex_sha1", u("{SHA}"), lazy=True)
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
576
passlib/handlers/scram.py
Normal file
576
passlib/handlers/scram.py
Normal 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
|
||||
#=============================================================================
|
150
passlib/handlers/sha1_crypt.py
Normal file
150
passlib/handlers/sha1_crypt.py
Normal 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
|
||||
#=============================================================================
|
486
passlib/handlers/sha2_crypt.py
Normal file
486
passlib/handlers/sha2_crypt.py
Normal 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
|
||||
#=============================================================================
|
364
passlib/handlers/sun_md5_crypt.py
Normal file
364
passlib/handlers/sun_md5_crypt.py
Normal 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
310
passlib/handlers/windows.py
Normal 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
28
passlib/hash.py
Normal 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
115
passlib/hosts.py
Normal 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
193
passlib/ifc.py
Normal 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
411
passlib/registry.py
Normal 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
|
||||
#=============================================================================
|
1
passlib/tests/__init__.py
Normal file
1
passlib/tests/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""passlib tests"""
|
6
passlib/tests/__main__.py
Normal file
6
passlib/tests/__main__.py
Normal file
@ -0,0 +1,6 @@
|
||||
import os
|
||||
from nose import run
|
||||
run(
|
||||
defaultTest=os.path.dirname(__file__),
|
||||
)
|
||||
|
15
passlib/tests/_test_bad_register.py
Normal file
15
passlib/tests/_test_bad_register.py
Normal 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
329
passlib/tests/backports.py
Normal 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
|
||||
#=============================================================================
|
9
passlib/tests/sample1.cfg
Normal file
9
passlib/tests/sample1.cfg
Normal 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
|
||||
|
9
passlib/tests/sample1b.cfg
Normal file
9
passlib/tests/sample1b.cfg
Normal 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
BIN
passlib/tests/sample1c.cfg
Normal file
Binary file not shown.
8
passlib/tests/sample_config_1s.cfg
Normal file
8
passlib/tests/sample_config_1s.cfg
Normal 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
|
564
passlib/tests/test_apache.py
Normal file
564
passlib/tests/test_apache.py
Normal 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
128
passlib/tests/test_apps.py
Normal 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
|
||||
#=============================================================================
|
1623
passlib/tests/test_context.py
Normal file
1623
passlib/tests/test_context.py
Normal file
File diff suppressed because it is too large
Load Diff
752
passlib/tests/test_context_deprecated.py
Normal file
752
passlib/tests/test_context_deprecated.py
Normal 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
|
||||
#=============================================================================
|
976
passlib/tests/test_ext_django.py
Normal file
976
passlib/tests/test_ext_django.py
Normal 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
|
||||
#=============================================================================
|
2217
passlib/tests/test_handlers.py
Normal file
2217
passlib/tests/test_handlers.py
Normal file
File diff suppressed because it is too large
Load Diff
472
passlib/tests/test_handlers_bcrypt.py
Normal file
472
passlib/tests/test_handlers_bcrypt.py
Normal 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
|
||||
#=============================================================================
|
366
passlib/tests/test_handlers_django.py
Normal file
366
passlib/tests/test_handlers_django.py
Normal 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
|
||||
#=============================================================================
|
98
passlib/tests/test_hosts.py
Normal file
98
passlib/tests/test_hosts.py
Normal 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
|
||||
#=============================================================================
|
214
passlib/tests/test_registry.py
Normal file
214
passlib/tests/test_registry.py
Normal 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
917
passlib/tests/test_utils.py
Normal 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
|
||||
#=============================================================================
|
599
passlib/tests/test_utils_crypto.py
Normal file
599
passlib/tests/test_utils_crypto.py
Normal 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
|
||||
#=============================================================================
|
806
passlib/tests/test_utils_handlers.py
Normal file
806
passlib/tests/test_utils_handlers.py
Normal 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
|
||||
#=============================================================================
|
51
passlib/tests/test_win32.py
Normal file
51
passlib/tests/test_win32.py
Normal 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
|
||||
#=============================================================================
|
83
passlib/tests/tox_support.py
Normal file
83
passlib/tests/tox_support.py
Normal 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
2252
passlib/tests/utils.py
Normal file
File diff suppressed because it is too large
Load Diff
1619
passlib/utils/__init__.py
Normal file
1619
passlib/utils/__init__.py
Normal file
File diff suppressed because it is too large
Load Diff
172
passlib/utils/_blowfish/__init__.py
Normal file
172
passlib/utils/_blowfish/__init__.py
Normal 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
|
||||
#=============================================================================
|
204
passlib/utils/_blowfish/_gen_files.py
Normal file
204
passlib/utils/_blowfish/_gen_files.py
Normal 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
|
||||
#=============================================================================
|
442
passlib/utils/_blowfish/base.py
Normal file
442
passlib/utils/_blowfish/base.py
Normal 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
|
||||
#=============================================================================
|
771
passlib/utils/_blowfish/unrolled.py
Normal file
771
passlib/utils/_blowfish/unrolled.py
Normal 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
436
passlib/utils/compat.py
Normal 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
859
passlib/utils/des.py
Normal 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
1664
passlib/utils/handlers.py
Normal file
File diff suppressed because it is too large
Load Diff
266
passlib/utils/md4.py
Normal file
266
passlib/utils/md4.py
Normal 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
415
passlib/utils/pbkdf2.py
Normal 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
68
passlib/win32.py
Normal 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
|
||||
#=============================================================================
|
Loading…
x
Reference in New Issue
Block a user