summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEli Collins <elic@assurancetechnologies.com>2011-01-28 06:35:56 +0000
committerEli Collins <elic@assurancetechnologies.com>2011-01-28 06:35:56 +0000
commitfea8e73c8e9bef3e9423af50c128cb20e7813b09 (patch)
tree984691ea244b83ff9cbc8d8692f66f00142f62b9
parentbc738f4c6e35a31c9edd5fb54f13773e15978a09 (diff)
downloadpasslib-fea8e73c8e9bef3e9423af50c128cb20e7813b09.tar.gz
wow. lots of rearranging
======================== * back to 1.2 structure * moved h64 helpers into utils.h64 module * pared down CryptHandler * tightened UTs somewhat
-rw-r--r--docs/crypt_handler_api.rst166
-rw-r--r--docs/notes.txt5
-rw-r--r--passlib/apache.py27
-rw-r--r--passlib/handler.py667
-rw-r--r--passlib/hash/__init__.py1
-rw-r--r--passlib/hash/__skel.py116
-rw-r--r--passlib/hash/apr_md5_crypt.py126
-rw-r--r--passlib/hash/bcrypt.py164
-rw-r--r--passlib/hash/des_crypt.py193
-rw-r--r--passlib/hash/ext_des_crypt.py173
-rw-r--r--passlib/hash/md5_crypt.py (renamed from passlib/unix/md5_crypt.py)208
-rw-r--r--passlib/hash/mysql_323.py84
-rw-r--r--passlib/hash/mysql_41.py76
-rw-r--r--passlib/hash/postgres_md5.py91
-rw-r--r--passlib/hash/sha256_crypt.py373
-rw-r--r--passlib/hash/sha512_crypt.py164
-rw-r--r--passlib/hash/sun_md5_crypt.py (renamed from passlib/unix/sun_md5_crypt.py)252
-rw-r--r--passlib/lanman.py39
-rw-r--r--passlib/mysql.py112
-rw-r--r--passlib/postgres.py61
-rw-r--r--passlib/tests/handler_utils.py15
-rw-r--r--passlib/tests/test_context.py2
-rw-r--r--passlib/tests/test_des_crypt.py190
-rw-r--r--passlib/tests/test_frontend.py8
-rw-r--r--passlib/tests/test_handler.py24
-rwxr-xr-xpasslib/tests/test_hash_bcrypt.py (renamed from passlib/tests/test_bcrypt.py)6
-rw-r--r--passlib/tests/test_hash_des_crypt.py139
-rw-r--r--passlib/tests/test_hash_md5_crypt.py (renamed from passlib/tests/test_md5_crypt.py)26
-rw-r--r--passlib/tests/test_hash_mysql.py (renamed from passlib/tests/test_mysql.py)9
-rw-r--r--passlib/tests/test_hash_postgres.py (renamed from passlib/tests/test_postgres.py)4
-rw-r--r--passlib/tests/test_hash_sha_crypt.py154
-rw-r--r--passlib/tests/test_hash_sun_md5_crypt.py (renamed from passlib/tests/test_sun_md5_crypt.py)4
-rw-r--r--passlib/tests/test_sha_crypt.py123
-rw-r--r--passlib/tests/test_utils.py120
-rw-r--r--passlib/tests/test_utils_pbkdf2.py6
-rw-r--r--passlib/tests/test_win32.py41
-rw-r--r--passlib/tests/utils.py10
-rw-r--r--passlib/unix.py (renamed from passlib/unix/__init__.py)20
-rw-r--r--passlib/unix/bcrypt.py148
-rw-r--r--passlib/unix/des_crypt.py247
-rw-r--r--passlib/unix/sha_crypt.py476
-rw-r--r--passlib/utils/__init__.py168
-rw-r--r--passlib/utils/_slow_des_crypt.py167
-rw-r--r--passlib/utils/des.py4
-rw-r--r--passlib/utils/h64.py145
-rw-r--r--passlib/win32.py54
46 files changed, 3110 insertions, 2298 deletions
diff --git a/docs/crypt_handler_api.rst b/docs/crypt_handler_api.rst
new file mode 100644
index 0000000..500b0eb
--- /dev/null
+++ b/docs/crypt_handler_api.rst
@@ -0,0 +1,166 @@
+======================
+api for crypt handlers
+======================
+
+Motivation
+==========
+Passlib supports many different password hashing schemes.
+A majority of them were originally designed to be used on a unix
+system, follow some variant of the unix ``crypt()`` api,
+and have are encoded using the Extended Crypt Format.
+Others were designed for use specific contexts only,
+such as PostgreSQL.
+
+Passlib was designed to provide a uniform interface to implementations
+of all these schemes, as well as hide away as much of the implementation
+detail as possible; both in order to make it easier to integrate password hashing
+into new and existing applications. Because of these goals, some of the methods
+required by the crypt handler api tend to overlap slightly,
+in order to accomodate a wide variety of application requirements,
+and other parts have been kept intentionally non-commital, in order to allow
+flexibility of implementation.
+
+All of the schemes built into passlib implement this interface;
+most them as modules within the :mod:`passlib.hash` package.
+
+Overview
+========
+A CryptHandler object may be a module, class, or instance.
+The only requirement is that it expose (at least) the following attributes
+and functions (for classes, the following functions must be static or class methods).
+
+CryptHandlers have the following three attributes:
+
+ * ``name`` - unique identifier used to distinguish scheme within
+ * ``setting_kwds`` - list of settings recognized by ``genconfig()`` and ``encrypt()``.
+ * ``context_kwds`` - list of context specified keywords required by algorithm
+
+CryptHandlers have the following five methods:
+
+ * ``genconfig(**settings) -> configuration string`` - used for generating configuration strings.
+ * ``genhash(secret, config, **context) -> hash`` - used for encrypting secret using configuration string or existing hash
+ * ``encrypt(secret, **context_and_settings) -> hash`` - used for encrypting secret using specified options
+ * ``identify(hash) -> True|False`` - used for identifying hash belonging to this algorithm
+ * ``verify(secret, hash, **context)`` - used for verifying a secret against an existing hash
+
+Usage Examples
+==============
+
+.. todo::
+
+ show some quick examples using bcrypt.
+
+Informational Attributes
+========================
+.. attribute:: name
+
+ A unique name used to identify
+ the particular algorithm this handler implements.
+
+ These names should consist only of lowercase a-z, the digits 0-9, and hyphens.
+
+ .. note::
+
+ All handlers built into passlib are implemented as modules
+ whose path corresponds to the name, with an underscore replacing the hyphen.
+ For example, ``des-crypt`` is stored as the module ``passlib.hash.des_crypt``.
+
+.. attribute:: setting_kwds
+
+ If the algorithm supports per-hash configuration
+ (such as salts, variable rounds, etc), this attribute
+ should contain a tuple of keywords corresponding
+ to each of those configuration options.
+
+ This should correspond with the keywords accepted
+ by :func:`genconfig`, see that method for details.
+
+ If no settings are supported, this attribute
+ is an empty tuple.
+
+.. attribute:: context_kwds
+
+ Some algorithms require external contextual information
+ in order to generate a checksum for a password.
+ An example of this is Postgres' md5 algorithm,
+ which requires the username to be provided
+ (which it uses as a salt).
+
+ This attribute should contain a tuple of keywords
+ which should be passed into :func:`encrypt`, :func:`verify`,
+ and :func:`genhash` in order to encrypt a password.
+
+ Since most password hashes require no external information,
+ this tuple will usually be empty.
+
+Primary Interface
+=================
+The ``encrypt()``, ``identify()``, and ``verify()`` methods are designed
+to provide an easy interface for applications to encrypt new passwords
+and verify existing passwords, without having to deal with details such
+as salt formats.
+
+.. autofunction:: encrypt
+.. autofunction:: identify
+.. autofunction:: verify
+
+Secondary Interface
+===================
+While the primary interface is generally the most useful when integrating
+password support into an application, those methods are for the most part
+built on top of the secondary interface, which is somewhat simpler
+for *implementing* new password schemes. It also happens to match
+the tradition unix crypt interface, and consists of two functions:
+``genconfig()`` and ``genhash``.
+
+.. autofunction:: genconfig
+.. autofunction:: genhash
+
+Other Methods
+=============
+Some of the CryptHandlers in passlib expose some additional function and attributes,
+which may be useful, but whose behavior varies between handlers (if present at all),
+and may not conform exactly to the following summary:
+
+.. autofunction:: parse
+
+ This method usually takes in a hash or configuration string
+ belonging to the scheme, and parses it into a dictionary
+ whose keys should match :attr:`setting_kwds`,
+ as well as the key ``checksum``, which is either ``None`` or
+ the encoded checksum portion of the string (ie, the hash itself).
+
+ It should raise :exc:`ValueError` in the same cases that :func:`genhash` would.
+
+ Most implementations of ``parse()`` do very little sanity checking,
+ leaving that job to ``genconfig``.
+
+.. autofunction:: render
+
+ This method is the inverse of :func:`parse`:
+ it takes in a dictionary such as returned by :func:`parse`,
+ and renders a hash or configuration string.
+
+ Most implementations of ``render()`` do very little sanity checking,
+ and may be willing to form strings which are malformed.
+
+For schemes which support a variable number of rounds,
+the following attributes are usually exposed:
+
+.. attribute:: default_rounds
+
+ The default number of rounds that will be used if not
+ explicitly set when calling :func:`encrypt` or :func:`genconfig`.
+
+.. attribute:: min_rounds
+
+ The minimum number of rounds the scheme allows.
+ Specifying values below this will generally result
+ in a warning, and ``min_rounds`` will be used instead.
+
+.. attribute:: max_rounds
+
+ The maximum number of rounds the scheme allows.
+ Specifying values above this will generally result
+ in a warning, and ``max_rounds`` will be used instead.
+
diff --git a/docs/notes.txt b/docs/notes.txt
index 5976c4d..9c18ce1 100644
--- a/docs/notes.txt
+++ b/docs/notes.txt
@@ -175,8 +175,9 @@ nt-hash
$3$hash
-
-http://search.cpan.org/~zefram/Authen-Passphrase-0.007/lib/Authen/Passphrase.pm
+references for hashes & passwords
+ http://cpansearch.perl.org/src/CHANSEN/Authen-Simple-0.4/lib/Authen/Simple/Password.pm
+ http://search.cpan.org/~zefram/Authen-Passphrase-0.007/lib/Authen/Passphrase.pm
======================================================================
OS notes
diff --git a/passlib/apache.py b/passlib/apache.py
index b12d9d1..6522b47 100644
--- a/passlib/apache.py
+++ b/passlib/apache.py
@@ -2,7 +2,32 @@
apache support
http://httpd.apache.org/docs/2.2/misc/password_encryptions.html
-
+http://httpd.apache.org/docs/2.0/programs/htpasswd.html
NOTE: digest format is md5(user ":" realm ":" passwd).hexdigest()
file is "user:realm:hash"
"""
+#=========================================================
+#imports
+#=========================================================
+from __future__ import with_statement
+#core
+import logging; log = logging.getLogger(__name__)
+#site
+#libs
+from passlib.hash import postgres_md5
+from passlib.context import CryptContext
+#pkg
+#local
+__all__ = [
+ 'postgres_md5',
+ 'postgres_context',
+]
+
+#=========================================================
+#db contexts
+#=========================================================
+postgres_context = CryptContext([postgres_md5])
+
+#=========================================================
+# eof
+#=========================================================
diff --git a/passlib/handler.py b/passlib/handler.py
index 79e5125..78cacdc 100644
--- a/passlib/handler.py
+++ b/passlib/handler.py
@@ -12,7 +12,7 @@ import time
import os
#site
#libs
-from passlib.utils import abstract_class_method, classproperty, H64_CHARS, getrandstr, rng, Undef
+from passlib.utils import abstract_class_method, classproperty, h64, getrandstr, rng, Undef
#pkg
#local
__all__ = [
@@ -32,15 +32,27 @@ __all__ = [
#=========================================================
#global registry
#=========================================================
-_handler_map = {} #dict mapping names & aliases -> crypt algorithm instances
-_name_set = set() #list of keys in _handler_map which are names not aliases
-def register_crypt_handler(obj):
+#list of builtin hashes (for list_crypt_handlers, to work around lazy loading)
+#XXX: could write some code in setup.py that generates this from package listing.
+_builtin_names = set([
+ "apr-md5-crypt", "bcrypt", "des-crypt", "ext-des-crypt",
+ "md5-crypt", "mysql-323", "mysql-41", "postgres-md5",
+ "sha256-crypt", "sha512-crypt", "sun-md5-crypt",
+ ])
+
+#dict mapping names & aliases -> loaded crypt algorithm handlers
+_handler_map = {}
+
+#list of keys in _handler_map which are names not aliases
+_name_set = set()
+
+def register_crypt_handler(obj, aliases=None):
"register CryptHandler handler"
global _handler_map, _name_set
if not is_crypt_handler(obj):
- raise TypeError, "object does not appear to be CryptHandler handler: %r" % (obj,)
+ raise TypeError, "object does not appear to be a CryptHandler: %r" % (obj,)
name = obj.name
_validate_name(name)
@@ -52,60 +64,89 @@ def register_crypt_handler(obj):
_handler_map[name] = obj
_name_set.add(name)
- for alias in obj.aliases:
- _validate_name(alias)
- if alias not in _name_set:
+ if aliases:
+ out = []
+ for alias in aliases:
+ if alias == name:
+ continue
+ if alias in _name_set:
+ continue
+ _validate_name(alias)
_handler_map[alias] = obj
+ out.append(alias)
- log.info("registered crypt handler: obj=%r name=%r aliases=%r", obj, obj.name, obj.aliases)
+ log.info("registered crypt handler: obj=%r name=%r aliases=%r", obj, obj.name, out)
+ else:
+ log.info("registered crypt handler: obj=%r name=%r", obj, obj.name)
def _validate_name(name):
"validate crypt algorithm name"
if not name:
- raise ValueError, "name/alias empty: %r" % (name,)
+ raise ValueError, "name is null: %r" % (name,)
if name.lower() != name:
- raise ValueError, "name/alias must be lower-case: %r" %(name,)
+ raise ValueError, "name must be lower-case: %r" %(name,)
if re.search("[^-a-zA-Z0-9]",name):
- raise ValueError, "names & aliases must consist of a-z, 0-9, A-Z: %r" % (name,)
+ raise ValueError, "names must consist of the characters -, a-z, A-Z, and 0-9: %r" % (name,)
return True
def get_crypt_handler(name, default=Undef):
"resolve crypt algorithm name / alias"
global _handler_map
- if default is Undef:
+
+ #check if handler loaded
+ handler = _handler_map.get(name)
+ if handler is not None:
+ return handler
+
+ #try to lazy load from passlib.hash.xxx
+ modname = name.replace("-","_")
+ try:
+ mod = __import__("passlib.hash." + modname, None, None, ['dummy'], 0)
+ except ImportError, err:
+ #make sure we don't hide failure to import dependancy
+ if str(err) != "No module named " + modname:
+ raise
+ else:
+ #module itself should be handler, so register it.
+ #if it was under a different name, treat that as an alias.
+ if getattr(mod,"name",None) != name:
+ aliases = (name,)
+ else:
+ aliases = ()
+ register_crypt_handler(mod, aliases)
+
+ #assume crypt handler loaded. error shouldn't happen here,
+ #since register_crypt_handler() should throw error if mod wasn't crypt handler.
return _handler_map[name]
+
+ #fail!
+ if default is Undef:
+ raise KeyError, "no crypt handler found for algorithm: %r" % (name,)
else:
- return _handler_map.get(name, default)
+ return default
-def list_crypt_handlerss():
+def list_crypt_handlers():
"return sorted list of all known crypt algorithm names"
- global _name_set
- return sorted(_name_set)
+ global _name_set, _builtin_names
+ return sorted(_name_set.union(_builtin_names))
#==========================================================
#other helpers
#==========================================================
def is_crypt_handler(obj):
"check if obj following CryptHandler protocol"
- #NOTE: this isn't an exhaustive check of all required attrs,
- #just a quick check of the most uniquely identifying ones
- return all(hasattr(obj, name) for name in (
- "name", "verify", "encrypt", "identify",
- ))
-
-def is_ext_crypt_handler(obj):
- "check if obj following ExtCryptHandler protocol"
- #NOTE: this isn't an exhaustive check of all required attrs,
- #just a quick check of the most uniquely identifying ones
return all(hasattr(obj, name) for name in (
- "name", "verify", "encrypt", "identify", "parse", "render"
+ "name",
+ "setting_kwds", "context_kwds",
+ "genconfig", "genhash",
+ "verify", "encrypt", "identify",
))
#==========================================================
#base interface for all the crypt algorithm implementations
#==========================================================
class CryptHandler(object):
- """base class for implementing a password algorithm.
+ """base class for implementing a password algorithm. see crypt handler api for details of structure.
Overview
========
@@ -131,46 +172,6 @@ class CryptHandler(object):
.. automethod:: genconfig
.. automethod:: genhash
-
-
- Informational Attributes
- ========================
- .. attribute:: name
-
- A unique name used to identify
- the particular algorithm this handler implements.
-
- These names should consist only of lowercase a-z, the digits 0-9, and underscores.
-
- Examples: ``"des_crypt"``, ``"md5_crypt"``.
-
- .. attribute:: setting_kwds
-
- If the algorithm supports per-hash configuration
- (such as salts, variable rounds, etc), this attribute
- should contain a tuple of keywords corresponding
- to each of those configuration options.
-
- This should correspond with the keywords accepted
- by that algorithm's :meth:`genconfig` method,
- see that method for details.
-
- If no settings are supported, this attribute
- should be an empty tuple.
-
- .. attribute:: context_kwds
-
- Some algorithms require external contextual information
- in order to generate a checksum for a password.
- An example of this is postgres' md5 algorithm,
- which requires the username to use as a salt.
-
- This attribute should contain a tuple of keywords
- which should be passed into :meth:`encrypt`, :meth:`verify`,
- and :meth:`genhash` in order to encrypt a password.
-
- Since most password hashes require no external information,
- this tuple will usually be empty.
"""
#=========================================================
@@ -178,8 +179,6 @@ class CryptHandler(object):
#=========================================================
name = None #globally unique name to identify algorithm. should be lower case and hyphens only
- aliases = () #optional list of aliases (other names) this hash should be recognized by
-
context_kwds = () #tuple of additional kwds required for any encrypt / verify operations; eg "realm" or "user"
setting_kwds = () #tuple of additional kwds that encrypt accepts for configuration algorithm; eg "salt" or "rounds"
@@ -188,7 +187,7 @@ class CryptHandler(object):
#=========================================================
@abstract_class_method
- def genhash(cls, secret, config, **context_kwds):
+ def genhash(cls, secret, config, **context):
"""encrypt secret to hash
Overview
@@ -297,17 +296,18 @@ class CryptHandler(object):
"""identify if a hash string belongs to this algorithm.
:arg hash:
- the hash string to check
+ the candidate hash string to check
:returns:
* ``True`` if input appears to be a hash string belonging to this algorithm.
* ``True`` if input appears to be a configuration string belonging to this algorithm.
* ``False`` if no input is specified
+ * ``False`` if none of the above conditions was met.
.. note::
Some handlers may or may not return ``True`` for malformed hashes.
- Those that do will raise a ValueError once the hash is passed to :meth:`genhash`.
- Most handlers, will just return ``False``.
+ Those that do will raise a ValueError once the hash is passed to :func:`verify`.
+ Most handlers, however, will just return ``False``.
"""
#NOTE: this default method is going to be *really* slow for most implementations,
#they should override it. but if genhash() conforms to the specification, this will do.
@@ -327,6 +327,7 @@ class CryptHandler(object):
:arg secret:
A string containing the secret to encode.
+
Unicode behavior is specified on a per-hash basis,
but the common case is to encode into utf-8
before processing.
@@ -365,7 +366,7 @@ class CryptHandler(object):
return cls.genhash(secret, config)
@classmethod
- def verify(cls, secret, hash, **context_kwds):
+ def verify(cls, secret, hash, **context):
"""verify a secret against an existing hash.
This checks if a secret matches against the one stored
@@ -381,9 +382,13 @@ class CryptHandler(object):
method. These should be limited to those listed
in :attr:`context_kwds`.
+ :raises TypeError:
+ * if the secret is not a string.
+
:raises ValueError:
* if the hash not specified
* if the hash does not match this algorithm's hash format
+ * if the provided secret contains forbidden chars (see :func:`encrypt`)
:returns:
``True`` if the secret matches, otherwise ``False``.
@@ -404,7 +409,7 @@ class CryptHandler(object):
raise ValueError, "not a %s hash" % (cls.name,)
#do simple string comparison
- return hash == cls.genhash(secret, hash, **context_kwds)
+ return hash == cls.genhash(secret, hash, **context)
#=========================================================
#eoc
@@ -413,299 +418,221 @@ class CryptHandler(object):
#=========================================================
#
#=========================================================
-class ExtCryptHandler(CryptHandler):
- """class providing an extended handler interface,
- allowing manipulation of hash & config strings.
-
- About
- -----
- this extended interface adds methods for parsing and rendering
- a hash or config string to / from a dictionary of components.
-
- this interface is generally easier to use when *implementing* hash
- algorithms, and as such is used through passlib. it's kept separate
- from :class:`CryptHandler` itself, since it's features are not typically
- required for user-facing purposes.
-
- Usage
- -----
- when implementing a hash algorithm...
-
- subclasses must implement:
-
- * parse()
- * render()
- * genconfig() - render usually helpful
- * genhash() - parse, render usually helpful
-
- subclasses may optionally implement more efficient versions of
- these functions, though the defaults should be sufficient:
-
- * identify() - requires parse()
- * verify() - requires parse()
-
- some helper methods are provided for implementing genconfig, genhash & verify.
- """
-
- #=========================================================
- #class attrs
- #=========================================================
-
- #---------------------------------------------------------
- # _norm_salt() configuration
- #---------------------------------------------------------
-
- salt_chars = None #fill in with (maxium) number of salt chars required, and _norm_salt() will handle truncating etc
- salt_charset = H64_CHARS #helper used when generating salt
- salt_charpat = None #optional regexp used by _norm_salt to validate salts
-
- #override only if minimum number of salt chars is different from salt_chars
- @classproperty
- def min_salt_chars(cls):
- return cls.salt_chars
-
- #---------------------------------------------------------
- #_norm_rounds() configuration
- #---------------------------------------------------------
- default_rounds = None #default number of rounds to use if none specified (can be name of a preset)
- min_rounds = None #minimum number of rounds (smaller values silently ignored)
- max_rounds = None #maximum number of rounds (larger values silently ignored)
-
- #=========================================================
- #backend parsing routines - used by helpers below
- #=========================================================
-
- @abstract_class_method
- def parse(cls, hash):
- """parse hash or config into dictionary.
-
- :arg hash: the hash/config string to parse
-
- :raises ValueError:
- If hash/config string is empty,
- or not recognized as belonging to this algorithm
-
- :returns:
- dictionary containing a subset of the keys
- specified in :attr:`setting_kwds`.
-
- commonly used keys are ``salt``, ``rounds``.
-
- If and only if the string is a hash, the dict should also contain
- the key ``checksum``, mapping to the checksum portion of the hash.
-
- .. note::
- Specific implementations may perform anywhere from none to full
- validation of input string; the primary goal of this method
- is to parse settings from single string into kwds
- which will be recognized by :meth:`render` and :meth:`encrypt`.
-
- :meth:`encrypt` is where validation of inputs *must* be performed.
-
- .. note::
- If multiple encoding formats are possible, this *must* normalize
- the checksum kwd to it's canonical format, so the default
- verify() method can work properly.
- """
-
- @abstract_class_method
- def render(cls, checksum, **settings):
- """render hash from checksum & settings (as returned by :meth:`parse`).
-
- :param checksum:
- Encoded checksum portion of hash.
-
- :param settings:
- All other keywords are algorithm-specified,
- and should be listed in :attr:`setting_kwds`.
-
- :raises ValueError:
- If any values are not encodeable into hash.
-
- :raises NotImplementedError:
- If checksum is omitted and the algorithm
- doesn't have any settings (:attr:`setting_kwds` is empty),
- or doesn't support generating "salt strings"
- which contain all configuration except for the
- checksum itself.
-
- :returns:
- if checksum is specified, this should return a fully-formed hash.
- otherwise, it should return a config string containing
- the specified inputs.
-
- .. note::
- Specific implementations may perform anywhere from none to full
- validation of inputs; the primary goal of this method
- is to render the settings into a single string
- which will be recognized by :meth:`parse`.
-
- :meth:`encrypt` is where validation of inputs *must* be performed.
- """
-
- #=========================================================
- #genhash helper functions
- #=========================================================
-
- #NOTE: genhash() must be implemented,
- # but helper functions are provided below for common workflows...
-
- #----------------------------------------------------------------
- #for handlers which normalize config string and hand off to external library
- #----------------------------------------------------------------
- @classmethod
- def _norm_config(cls, config):
- """normalize & validate config string"""
- assert cls.setting_kwds, "_norm_config not designed for hashses w/o settings"
- if not config:
- raise ValueError, "no %s hash or config string specified" % (cls.name,)
- settings = cls.parse(config) #this should catch malformed entries
- settings.pop("checksum", None) #remove checksum if a hash was passed in
- return cls.genconfig(**settings) #re-generate config string, let genconfig() catch invalid values
-
- #----------------------------------------------------------------
- #for handlers which implement the guts of the process directly
- #----------------------------------------------------------------
-
- # render() is also usually used for implementing genhash() in this case
-
- @classmethod
- def _parse_norm_config(cls, config):
- """normalize & validate config string, return parsed dictionary"""
- return cls.parse(cls._norm_config(config))
-
- #=========================================================
- #genconfig helpers
- #=========================================================
-
- #NOTE: genconfig() must still be implemented,
- # but helper functions provided below
-
- #render() is usually used for implementing genconfig()
-
- #----------------------------------------------------------------
- #normalization helpers rounds
- #----------------------------------------------------------------
- @classmethod
- def _norm_rounds(cls, rounds):
- """helper routine for normalizing rounds
-
- * falls back to :attr:`default_rounds`
- * raises ValueError if no fallback
- * clips to min_rounds / max_rounds
- * issues warnings if rounds exists min/max
-
- :returns: normalized rounds value
- """
- if not rounds:
- rounds = cls.default_rounds
- if not rounds:
- raise ValueError, "rounds must be specified explicitly"
- mx = cls.max_rounds
- if mx and rounds > mx:
- warn("%s algorithm does not allow more than %d rounds: %d", mx, rounds)
- rounds = mx
- mn = cls.min_rounds
- if mn and rounds < mn:
- warn("%s algorithm does not allow less than %d rounds: %d", mn, rounds)
- rounds = mn
- return rounds
-
- #----------------------------------------------------------------
- #normalization helpers for salts
- #----------------------------------------------------------------
- @classmethod
- def _gen_salt(cls):
- """helper routine to generate salt, used by _norm_salt"""
- return getrandstr(rng, cls.salt_charset, cls.salt_chars)
-
- @classmethod
- def _validate_salt_chars(cls, salt):
- "validate chars in salt, used by _norm_salt"
- cs = cls.salt_charset
- for c in salt:
- if c not in cs:
- raise ValueError, "invalid character in %s salt: %r" % (cls.name, c)
- return salt
-
- @classmethod
- def _norm_salt(cls, salt):
- """helper routine for normalizing salt
-
- required salt_charset & salt_chars attrs to be filled in,
- along with optional min_salt_chars attr (defaults to salt_chars).
-
- * generates salt if none provided
- * clips salt to maximum length of salt_chars
-
- :raises ValueError:
- * if salt contains chars that aren't in salt_charset.
- * if salt contains less than min_salt_chars characters.
-
- :returns:
- resulting or generated salt
- """
- if salt is None:
- return cls._gen_salt()
-
- salt = cls._validate_salt_chars(salt)
-
- mn = cls.min_salt_chars
- assert mn is not None, "cls.min_salt_chars not set"
- if len(salt) < mn:
- raise ValueError, "%s salt must be at least %d chars" % (cls.name, mn)
-
- mx = cls.salt_chars
- assert mx is not None, "cls.salt_chars not set"
- if len(salt) > mx:
- #automatically clip things to specified number of chars
- return salt[:mx]
- else:
- return salt
-
- #=========================================================
- #identify helpers
- #=========================================================
-
- #NOTE: this default identify implementation is usually sufficient
- # (and better than CryptHandler.identify),
- # though implementations may override it with an even faster check,
- # such as just looking for a specific string prefix & size
-
- @classmethod
- def identify(cls, hash):
- try:
- cls.parse(hash)
- except ValueError:
- return False
- return True
-
- #=========================================================
- #encrypt helper functions
- #=========================================================
-
- #NOTE: the default encrypt() method very rarely needs overidding at all.
-
- #=========================================================
- #verify helper functions
- #=========================================================
-
- #NOTE: the default verify method provided here works for most cases,
- # though some handlers will want to implement norm_hash() if their
- # hash has multiple equivalent representations (eg: case insensitive)
-
- @classmethod
- def verify(cls, secret, hash, **context_kwds):
- info = cls.parse(hash) #<- should throw ValueError for us if hash is invalid
- if not info.get('checksum'):
- raise ValueError, "hash lacks checksum (did you pass a config string into verify?)"
- other_hash = cls.genhash(secret, hash, **context_kwds)
- other_info = cls.parse(other_hash)
- return info['checksum'] == other_info['checksum']
-
- #=========================================================
- #eoc
- #=========================================================
+##class ExtCryptHandler(CryptHandler):
+## """class providing an extended handler interface,
+## allowing manipulation of hash & config strings.
+##
+## this extended interface adds methods for parsing and rendering
+## a hash or config string to / from a dictionary of components.
+##
+## this interface is generally easier to use when *implementing* hash
+## algorithms, and as such is used through passlib. it's kept separate
+## from :class:`CryptHandler` itself, since it's features are not typically
+## required for user-facing purposes.
+##
+## when implementing a hash algorithm, subclasses must implement:
+##
+## * parse()
+## * render()
+## * genconfig() - render, _norm_salt, _norm_rounds usually helpful for this
+## * genhash() - parse, render usually helpful for this
+##
+## subclasses may optionally implement more efficient versions of
+## these functions, though the defaults should be sufficient:
+##
+## * identify() - requires parse()
+## * verify() - requires parse()
+##
+## some helper methods are provided for implementing genconfig, genhash & verify.
+## """
+##
+## #=========================================================
+## #class attrs
+## #=========================================================
+##
+## #---------------------------------------------------------
+## # _norm_salt() configuration
+## #---------------------------------------------------------
+##
+## salt_chars = None #fill in with (maxium) number of salt chars required, and _norm_salt() will handle truncating etc
+## salt_charset = h64.CHARS #helper used when generating salt
+## salt_charpat = None #optional regexp used by _norm_salt to validate salts
+##
+## #override only if minimum number of salt chars is different from salt_chars
+## @classproperty
+## def min_salt_chars(cls):
+## return cls.salt_chars
+##
+## #---------------------------------------------------------
+## #_norm_rounds() configuration
+## #---------------------------------------------------------
+## default_rounds = None #default number of rounds to use if none specified (can be name of a preset)
+## min_rounds = None #minimum number of rounds (smaller values silently ignored)
+## max_rounds = None #maximum number of rounds (larger values silently ignored)
+##
+## #=========================================================
+## #backend parsing routines - used by helpers below
+## #=========================================================
+##
+## @abstract_class_method
+## def parse(cls, hash):
+## """parse hash or config into dictionary.
+##
+## :arg hash: the hash/config string to parse
+##
+## :raises ValueError:
+## If hash/config string is empty,
+## or not recognized as belonging to this algorithm
+##
+## :returns:
+## dictionary containing a subset of the keys
+## specified in :attr:`setting_kwds`.
+##
+## commonly used keys are ``salt``, ``rounds``.
+##
+## If and only if the string is a hash, the dict should also contain
+## the key ``checksum``, mapping to the checksum portion of the hash.
+##
+## .. note::
+## Specific implementations may perform anywhere from none to full
+## validation of input string; the primary goal of this method
+## is to parse settings from single string into kwds
+## which will be recognized by :meth:`render` and :meth:`encrypt`.
+##
+## :meth:`encrypt` is where validation of inputs *must* be performed.
+##
+## .. note::
+## If multiple encoding formats are possible, this *must* normalize
+## the checksum kwd to it's canonical format, so the default
+## verify() method can work properly.
+## """
+##
+## @abstract_class_method
+## def render(cls, checksum=None, **settings):
+## """render hash from checksum & settings (as returned by :meth:`parse`).
+##
+## :param checksum:
+## Encoded checksum portion of hash.
+##
+## :param settings:
+## All other keywords are algorithm-specified,
+## and should be listed in :attr:`setting_kwds`.
+##
+## :raises ValueError:
+## If any values are not encodeable into hash.
+##
+## :raises NotImplementedError:
+## If checksum is omitted and the algorithm
+## doesn't have any settings (:attr:`setting_kwds` is empty),
+## or doesn't support generating "salt strings"
+## which contain all configuration except for the
+## checksum itself.
+##
+## :returns:
+## if checksum is specified, this should return a fully-formed hash.
+## otherwise, it should return a config string containing
+## the specified inputs.
+##
+## .. note::
+## Specific implementations may perform anywhere from none to full
+## validation of inputs; the primary goal of this method
+## is to render the settings into a single string
+## which will be recognized by :meth:`parse`.
+##
+## :meth:`encrypt` is where validation of inputs *must* be performed.
+## """
+##
+## #=========================================================
+## #genhash helper functions
+## #=========================================================
+##
+## #NOTE: genhash() must be implemented,
+## # but helper functions are provided below for common workflows...
+##
+## #----------------------------------------------------------------
+## #for handlers which normalize config string and hand off to external library
+## #----------------------------------------------------------------
+## @classmethod
+## def _norm_config(cls, config):
+## """normalize & validate config string"""
+## assert cls.setting_kwds, "_norm_config not designed for hashses w/o settings"
+## if not config:
+## raise ValueError, "no %s hash or config string specified" % (cls.name,)
+## settings = cls.parse(config) #this should catch malformed entries
+## settings.pop("checksum", None) #remove checksum if a hash was passed in
+## return cls.genconfig(**settings) #re-generate config string, let genconfig() catch invalid values
+##
+## #----------------------------------------------------------------
+## #for handlers which implement the guts of the process directly
+## #----------------------------------------------------------------
+##
+## # render() is also usually used for implementing genhash() in this case
+##
+## @classmethod
+## def _parse_norm_config(cls, config):
+## """normalize & validate config string, return parsed dictionary"""
+## return cls.parse(cls._norm_config(config))
+##
+## #=========================================================
+## #genconfig helpers
+## #=========================================================
+##
+## #NOTE: genconfig() must still be implemented,
+## # but helper functions provided below
+##
+## #render() is usually used for implementing genconfig()
+##
+## @classmethod
+## def _norm_rounds(cls, rounds):
+## return norm_rounds(rounds, cls.default_rounds, cls.min_rounds, cls.max_rounds, name=cls.name)
+##
+## @classmethod
+## def _norm_salt(cls, salt):
+## return norm_salt(salt, cls.min_salt_chars, cls.salt_chars, cls.salt_charset, name=cls.name)
+##
+## #=========================================================
+## #identify helpers
+## #=========================================================
+##
+## #NOTE: this default identify implementation is usually sufficient
+## # (and better than CryptHandler.identify),
+## # though implementations may override it with an even faster check,
+## # such as just looking for a specific string prefix & size
+##
+## @classmethod
+## def identify(cls, hash):
+## try:
+## cls.parse(hash)
+## except ValueError:
+## return False
+## return True
+##
+## #=========================================================
+## #encrypt helper functions
+## #=========================================================
+##
+## #NOTE: the default encrypt() method very rarely needs overidding at all.
+##
+## #=========================================================
+## #verify helper functions
+## #=========================================================
+##
+## #NOTE: the default verify method provided here works for most cases,
+## # though some handlers will want to implement norm_hash() if their
+## # hash has multiple equivalent representations (eg: case insensitive)
+##
+## @classmethod
+## def verify(cls, secret, hash, **context_kwds):
+## info = cls.parse(hash) #<- should throw ValueError for us if hash is invalid
+## if not info.get('checksum'):
+## raise ValueError, "hash lacks checksum (did you pass a config string into verify?)"
+## other_hash = cls.genhash(secret, hash, **context_kwds)
+## other_info = cls.parse(other_hash)
+## return info['checksum'] == other_info['checksum']
+##
+## #=========================================================
+## #eoc
+## #=========================================================
#=========================================================
# eof
diff --git a/passlib/hash/__init__.py b/passlib/hash/__init__.py
new file mode 100644
index 0000000..025b6d2
--- /dev/null
+++ b/passlib/hash/__init__.py
@@ -0,0 +1 @@
+#XXX: make this a namespace package ?
diff --git a/passlib/hash/__skel.py b/passlib/hash/__skel.py
new file mode 100644
index 0000000..bd2ddc8
--- /dev/null
+++ b/passlib/hash/__skel.py
@@ -0,0 +1,116 @@
+"""passlib.hash._skel - skeleton file for creating new hash modules
+"""
+#=========================================================
+#imports
+#=========================================================
+#core
+import re
+import logging; log = logging.getLogger(__name__)
+from warnings import warn
+#site
+#libs
+from passlib.utils import norm_rounds, norm_salt
+#pkg
+#local
+__all__ = [
+ "genhash",
+ "genconfig",
+ "encrypt",
+ "identify",
+ "verify",
+]
+
+#=========================================================
+#backend
+#=========================================================
+
+#=========================================================
+#algorithm information
+#=========================================================
+name = "xxx"
+#stats: ??? bit checksum, ??? bit salt, ??? rounds, max ??? chars of secret
+
+setting_kwds = ("salt", "rounds")
+context_kwds = ()
+
+default_rounds = None #current passlib default
+min_rounds = 1
+max_rounds = 1
+
+#=========================================================
+#internal helpers
+#=========================================================
+_pat = re.compile(r"""
+ ^
+ \$xxx
+ \$(?P<rounds>\d+)
+ \$(?P<salt>[A-Za-z0-9./]{xxx})
+ (\$(?P<chk>[A-Za-z0-9./]{xxx})?)?
+ $
+ """, re.X)
+
+def parse(hash):
+ if not hash:
+ raise ValueError, "no hash specified"
+ m = _pat.match(hash)
+ if not m:
+ raise ValueError, "invalid xxx hash"
+ rounds, salt, chk = m.group("rounds", "salt", "chk")
+ return dict(
+ rounds=int(rounds),
+ salt=salt,
+ checksum=chk,
+ )
+
+def render(rounds, salt, checksum=None):
+ return "$xxx$%d$%s$%s" % (rounds, salt, checksum or '')
+
+#=========================================================
+#primary interface
+#=========================================================
+def genconfig(salt=None, rounds=None):
+ """generate xxx configuration string
+
+ :param salt:
+ optional salt string to use.
+
+ if omitted, one will be automatically generated (recommended).
+
+ length must be XXX characters.
+ characters must be in range ``A-Za-z0-9./``.
+
+ :param rounds:
+
+ optional number of rounds, must be between XXX and XXX inclusive.
+
+ :returns:
+ xxx configuration string.
+ """
+ salt = norm_salt(salt, 22, name=name)
+ rounds = norm_rounds(rounds, default_rounds, min_rounds, max_rounds, name=name)
+ return render(rounds, salt, None)
+
+def genhash(secret, config):
+ #parse and run through genconfig to validate configuration
+ info = parse(config)
+ info.pop("checksum")
+ config = genconfig(**info)
+
+ #run through chosen backend
+ return bcrypt(secret, config)
+
+#=========================================================
+#secondary interface
+#=========================================================
+def encrypt(secret, **settings):
+ return genhash(secret, genconfig(**settings))
+
+def verify(secret, hash):
+ return hash == genhash(secret, hash)
+
+def identify(hash):
+ return bool(hash and _pat.match(hash))
+
+#=========================================================
+#eof
+#=========================================================
diff --git a/passlib/hash/apr_md5_crypt.py b/passlib/hash/apr_md5_crypt.py
new file mode 100644
index 0000000..b7e0747
--- /dev/null
+++ b/passlib/hash/apr_md5_crypt.py
@@ -0,0 +1,126 @@
+"""passlib.hash.apr_md5_crypt - Apache variant of md5-crypt algorithm
+
+This format is primarily used by Apache in htpasswd files.
+
+.. note::
+ This format would be identical to md5-crypt,
+ except for two things: it uses ``$apr1$`` as it's prefix
+ when encoded, and inserts that constant into the hash calculation
+ where md5-crypt would insert ``$1$``.
+ Thus, the formats aren't compatible, nor the checksums they contain.
+ Other than that, they have identical levels of security.
+"""
+#=========================================================
+#imports
+#=========================================================
+#core
+import re
+import logging; log = logging.getLogger(__name__)
+from warnings import warn
+#site
+#libs
+from passlib.utils import norm_rounds, norm_salt
+#pkg
+from passlib.hash.md5_crypt import raw_md5_crypt
+#local
+__all__ = [
+ "genhash",
+ "genconfig",
+ "encrypt",
+ "identify",
+ "verify",
+]
+
+#=========================================================
+#choose backend
+#=========================================================
+
+#TODO: could check for libssl support (openssl passwd -apr1)
+
+#=========================================================
+#algorithm information
+#=========================================================
+name = "apr-md5-crypt"
+#stats: 96 bit checksum, 48 bit salt
+
+setting_kwds = ("salt",)
+context_kwds = ()
+
+#=========================================================
+#internal helpers
+#=========================================================
+_pat = re.compile(r"""
+ ^
+ \$apr1
+ \$(?P<salt>[A-Za-z0-9./]{,8})
+ (\$(?P<chk>[A-Za-z0-9./]{22})?)?
+ $
+ """, re.X)
+
+def parse(hash):
+ if not hash:
+ raise ValueError, "no hash specified"
+ m = _pat.match(hash)
+ if not m:
+ raise ValueError, "invalid apr-md5-crypt hash"
+ salt, chk = m.group("salt", "chk")
+ return dict(
+ salt=salt,
+ checksum=chk,
+ )
+
+def render(salt, checksum=None):
+ return "$apr1$%s$%s" % (salt, checksum or '')
+
+#=========================================================
+#primary interface
+#=========================================================
+def genconfig(salt=None, rounds=None):
+ """generate apr-md5-crypt configuration string
+
+ :param salt:
+ optional salt string to use.
+
+ if omitted, one will be automatically generated (recommended).
+
+ length must be between 0 and 8 characters inclusive.
+ characters must be in range ``A-Za-z0-9./``.
+
+ :returns:
+ md5-crypt configuration string.
+ """
+ salt = norm_salt(salt, 0, 8, name=name)
+ return render(salt, None)
+
+def genhash(secret, config):
+ #parse and run through genconfig to validate configuration
+ #FIXME: could eliminate an extra render+parse call here
+ info = parse(config)
+ info.pop("checksum")
+ config = genconfig(**info)
+ info = parse(config)
+
+ #FIXME: can't find definitive policy on how md5-crypt handles non-ascii.
+ if isinstance(secret, unicode):
+ secret = secret.encode("utf-8")
+
+ #run through chosen backend
+ salt = info['salt']
+ checksum = raw_md5_crypt(secret, salt, apr=True)
+ return render(salt, checksum)
+
+#=========================================================
+#secondary interface
+#=========================================================
+def encrypt(secret, **settings):
+ return genhash(secret, genconfig(**settings))
+
+def verify(secret, hash):
+ return hash == genhash(secret, hash)
+
+def identify(hash):
+ return bool(hash and _pat.match(hash))
+
+#=========================================================
+#eof
+#=========================================================
diff --git a/passlib/hash/bcrypt.py b/passlib/hash/bcrypt.py
new file mode 100644
index 0000000..e8df49e
--- /dev/null
+++ b/passlib/hash/bcrypt.py
@@ -0,0 +1,164 @@
+"""passlib.bcrypt
+
+Implementation of OpenBSD's BCrypt algorithm.
+
+Passlib will use the py-bcrypt package if it is available,
+otherwise it will fall back to a slower builtin pure-python implementation.
+
+Note that rounds must be >= 10 or an error will be returned.
+"""
+#=========================================================
+#imports
+#=========================================================
+from __future__ import with_statement, absolute_import
+#core
+import re
+import logging; log = logging.getLogger(__name__)
+from warnings import warn
+#site
+#libs
+from passlib.utils import norm_rounds, norm_salt
+#pkg
+#local
+__all__ = [
+ "BCrypt",
+## "bcrypt", "backend",
+]
+
+#=========================================================
+#backend
+#=========================================================
+#fall back to our much slower pure-python implementation
+from passlib.utils._slow_bcrypt import hashpw as bcrypt
+backend = "builtin"
+
+try:
+ #try importing py-bcrypt, it's much faster
+ from bcrypt import hashpw as bcrypt
+ backend = "pybcrypt"
+except ImportError:
+ #check for OS crypt support before falling back to pure python version
+ try:
+ from crypt import crypt
+ except ImportError:
+ pass
+ else:
+ if (
+ crypt("test", "$2a$04$......................") == '$2a$04$......................qiOQjkB8hxU8OzRhS.GhRMa4VUnkPty'
+ and
+ crypt("test", "$2$04$......................") == '$2$04$......................1O4gOrCYaqBG3o/4LnT2ykQUt1wbyju'
+ ):
+ def bcrypt(secret, config):
+ if isinstance(secret, unicode):
+ secret = secret.encode("utf-8")
+ hash = crypt(secret, config)
+ if not hash.startswith("$2a$") and not hash.startswith("$2$"):
+ #means config was wrong
+ raise ValueError, "not a bcrypt hash"
+ return hash
+ backend = "stdlib"
+
+#XXX: should issue warning when _slow_bcrypt is first used.
+
+#=========================================================
+#algorithm information
+#=========================================================
+name = "bcrypt"
+#stats: 192 bit checksum, 128 bit salt, 2**(4..31) rounds, max 72 chars of secret
+
+setting_kwds = ("salt", "rounds")
+context_kwds = ()
+
+default_rounds = 12 #current passlib default
+min_rounds = 4 # bcrypt spec specified minimum
+max_rounds = 31 # 32-bit integer limit (real_rounds=1<<rounds)
+
+#=========================================================
+#internal helpers
+#=========================================================
+_pat = re.compile(r"""
+ ^
+ \$(?P<ident>2a?)
+ \$(?P<rounds>\d+)
+ \$(?P<salt>[A-Za-z0-9./]{22})
+ (?P<chk>[A-Za-z0-9./]{31})?
+ $
+ """, re.X)
+
+def parse(hash):
+ if not hash:
+ raise ValueError, "no hash specified"
+ m = _pat.match(hash)
+ if not m:
+ raise ValueError, "invalid bcrypt hash"
+ ident, rounds, salt, chk = m.group("ident", "rounds", "salt", "chk")
+ out = dict(
+ rounds=int(rounds),
+ salt=salt,
+ checksum=chk,
+ )
+ if ident == '2':
+ out['omit_null_suffix'] = True
+ return out
+
+def render(rounds, salt, checksum=None, omit_null_suffix=False):
+ if omit_null_suffix:
+ out = "$2$%d$%s" % (rounds, salt)
+ else:
+ out = "$2a$%d$%s" % (rounds, salt)
+ if checksum is not None:
+ out += "$" + checksum
+ return out
+
+#=========================================================
+#primary interface
+#=========================================================
+def genconfig(salt=None, rounds=None, omit_null_suffix=False):
+ """generate bcrypt configuration string
+
+ :param salt:
+ optional salt string to use.
+
+ if omitted, one will be automatically generated (recommended).
+
+ length must be 22 characters.
+ characters must be in range ``A-Za-z0-9./``.
+
+ :param rounds:
+
+ optional number of rounds, must be between 4 and 31 inclusive.
+
+ unlike most algorithms, bcrypt's rounds value is logarithmic,
+ each increase of +1 will double the actual number of rounds used.
+
+ :returns:
+ bcrypt configuration string.
+ """
+ salt = norm_salt(salt, 22, name=name)
+ rounds = norm_rounds(rounds, default_rounds, min_rounds, max_rounds, name=name)
+ return render(rounds, salt, None, omit_null_suffix)
+
+def genhash(secret, config):
+ #parse and run through genconfig to validate configuration
+ info = parse(config)
+ info.pop("checksum")
+ config = genconfig(**info)
+
+ #run through chosen backend
+ return bcrypt(secret, config)
+
+#=========================================================
+#secondary interface
+#=========================================================
+def encrypt(secret, **settings):
+ return genhash(secret, genconfig(**settings))
+
+def verify(secret, hash):
+ return hash == genhash(secret, hash)
+
+def identify(hash):
+ return bool(hash and _pat.match(hash))
+
+#=========================================================
+#eof
+#=========================================================
diff --git a/passlib/hash/des_crypt.py b/passlib/hash/des_crypt.py
new file mode 100644
index 0000000..2389e46
--- /dev/null
+++ b/passlib/hash/des_crypt.py
@@ -0,0 +1,193 @@
+"""passlib.hash.des_crypt - traditional unix (DES) crypt
+
+Old Unix-Crypt Algorithm, as originally used on unix before md5-crypt arrived.
+This implementation uses the builtin ``crypt`` module when available,
+but contains a pure-python fallback so that this algorithm can always be used.
+"""
+#references -
+# http://www.phpbuilder.com/manual/function.crypt.php
+# http://dropsafe.crypticide.com/article/1389
+
+#=========================================================
+#imports
+#=========================================================
+#core
+import re
+import logging; log = logging.getLogger(__name__)
+from warnings import warn
+#site
+#libs
+from passlib.utils import norm_salt, h64
+from passlib.utils.des import mdes_encrypt_int_block
+#pkg
+#local
+__all__ = [
+ "genhash",
+ "genconfig",
+ "encrypt",
+ "identify",
+ "verify",
+]
+
+#=========================================================
+#pure-python backend
+#=========================================================
+def _crypt_secret_to_key(secret):
+ "hash secret -> key using crypt format"
+ key_value = 0
+ for i, c in enumerate(secret[:8]):
+ key_value |= (ord(c)&0x7f) << (57-8*i)
+ return key_value
+
+def raw_crypt(secret, salt):
+ "pure-python fallback if stdlib support not present"
+ assert len(salt) == 2
+
+ #NOTE: technically might be able to use
+ #fewer salt chars, not sure what standard behavior is,
+ #so forbidding it for handler.
+
+ try:
+ salt_value = h64.decode_int12(salt)
+ except ValueError:
+ raise ValueError, "invalid chars in salt"
+ #FIXME: ^ this will throws error if bad salt chars are used
+ # whereas linux crypt does something (inexplicable) with it
+
+ #convert secret string into an integer
+ key_value = _crypt_secret_to_key(secret)
+
+ #run data through des using input of 0
+ result = mdes_encrypt_int_block(key_value, 0, salt=salt_value, rounds=25)
+
+ #run h64 encode on result
+ return h64.encode_int64(result)
+
+#=========================================================
+#choose backend
+#=========================================================
+backend = "builtin"
+
+try:
+ #try stdlib module, which is only present under posix
+ from crypt import crypt
+ if crypt("test", "ab") == 'abgOeLfPimXQo':
+ backend = "os-crypt"
+ else:
+ #shouldn't be any unix os which has crypt but doesn't support this format.
+ warn("crypt() failed runtime test for DES-CRYPT support")
+ crypt = None
+except ImportError:
+ #XXX: could check for openssl passwd -des support in libssl
+
+ #TODO: need to reconcile our implementation's behavior
+ # with the stdlib's behavior so error types, messages, and limitations
+ # are the same. (eg: handling of None and unicode chars)
+ crypt = None
+
+#=========================================================
+#algorithm information
+#=========================================================
+name = "des-crypt"
+#stats: 66 bit checksum, 12 bit salt, max 8 chars of secret
+
+setting_kwds = ("salt",)
+context_kwds = ()
+
+#=========================================================
+#internal helpers
+#=========================================================
+#FORMAT: 2 chars of H64-encoded salt + 11 chars of H64-encoded checksum
+_pat = re.compile(r"""
+ ^
+ (?P<salt>[./a-z0-9]{2})
+ (?P<chk>[./a-z0-9]{11})?
+ $""", re.X|re.I)
+
+def parse(hash):
+ if not hash:
+ raise ValueError, "no hash specified"
+ m = _pat.match(hash)
+ if not m:
+ raise ValueError, "invalid des-crypt hash"
+ salt, chk = m.group("salt", "chk")
+ return dict(
+ salt=salt,
+ checksum=chk,
+ )
+
+def render(salt, checksum=None):
+ if len(salt) < 2:
+ raise ValueError, "invalid salt"
+ return "%s%s" % (salt[:2], checksum or '')
+
+#=========================================================
+#primary interface
+#=========================================================
+def genconfig(salt=None):
+ """generate xxx configuration string
+
+ :param salt:
+ optional salt string to use.
+
+ if omitted, one will be automatically generated (recommended).
+
+ length must be 2 characters.
+ characters must be in range ``A-Za-z0-9./``.
+
+ :returns:
+ xxx configuration string.
+ """
+ salt = norm_salt(salt, 2, name=name)
+ return render(salt, None)
+
+def genhash(secret, config):
+ #parse and run through genconfig to validate configuration
+ info = parse(config)
+ info.pop("checksum")
+ config = genconfig(**info)
+
+ #forbidding nul chars because linux crypt (and most C implementations) won't accept it either.
+ if '\x00' in secret:
+ raise ValueError, "null char in secret"
+
+ #XXX: des-crypt predates unicode, not sure if there's an official policy for handing it.
+ #for now, just coercing to utf-8.
+ if isinstance(secret, unicode):
+ secret = secret.encode("utf-8")
+
+ #run through chosen backend
+ if crypt:
+ #XXX: given a single letter salt, linux crypt returns a hash with the original salt doubled,
+ # but appears to calculate the hash based on the letter + "G" as the second byte.
+ # this results in a hash that won't validate, which is DEFINITELY wrong.
+ # need to find out it's underlying logic, and if it's part of spec,
+ # or just weirdness that should actually be an error.
+ # until then, passlib raises an error in genconfig()
+
+ #XXX: given salt chars outside of h64.CHARS range, linux crypt
+ # does something unknown when decoding salt to 12 bit int,
+ # successfully creates a hash, but reports the original salt.
+ # need to find out it's underlying logic, and if it's part of spec,
+ # or just weirdness that should actually be an error.
+ # until then, passlib raises an error for bad salt chars.
+ return crypt(secret, config)
+ else:
+ salt = config[:2]
+ return render(salt, raw_crypt(secret, salt))
+
+#=========================================================
+#secondary interface
+#=========================================================
+def encrypt(secret, **settings):
+ return genhash(secret, genconfig(**settings))
+
+def verify(secret, hash):
+ return hash == genhash(secret, hash)
+
+def identify(hash):
+ return bool(hash and _pat.match(hash))
+
+#=========================================================
+#eof
+#=========================================================
diff --git a/passlib/hash/ext_des_crypt.py b/passlib/hash/ext_des_crypt.py
new file mode 100644
index 0000000..0389821
--- /dev/null
+++ b/passlib/hash/ext_des_crypt.py
@@ -0,0 +1,173 @@
+"""passlib.hash.ext_des_crypt - extended BSDi unix (DES) crypt
+
+this algorithm was used on some systems
+during the time between the original crypt()
+and the development of md5-crypt and the modular crypt format.
+
+thus, it doesn't follow the normal format,
+but it does enhance the crypt algorithm to include
+all chars, and adds a rounds parameter.
+
+References
+----------
+http://fuse4bsd.creo.hu/localcgi/man-cgi.cgi?crypt+3
+http://search.cpan.org/dist/Authen-Passphrase/lib/Authen/Passphrase/DESCrypt.pm
+
+"""
+#=========================================================
+#imports
+#=========================================================
+#core
+import re
+import logging; log = logging.getLogger(__name__)
+from warnings import warn
+#site
+#libs
+from passlib.utils import norm_rounds, norm_salt, h64
+from passlib.utils.des import mdes_encrypt_int_block
+from passlib.hash.des_crypt import _crypt_secret_to_key
+#pkg
+#local
+__all__ = [
+ "genhash",
+ "genconfig",
+ "encrypt",
+ "identify",
+ "verify",
+]
+
+#=========================================================
+#backend
+#=========================================================
+def raw_ext_crypt(secret, rounds, salt):
+ "ext_crypt() helper which returns checksum only"
+
+ #decode salt
+ try:
+ salt_value = h64.decode_int24(salt)
+ except ValueError:
+ raise ValueError, "invalid salt"
+
+ #validate secret
+ if '\x00' in secret:
+ #builtin linux crypt doesn't like this, so we don't either
+ #XXX: would make more sense to raise ValueError, but want to be compatible w/ stdlib crypt
+ raise ValueError, "secret must be string without null bytes"
+
+ #XXX: doesn't match stdlib, but just to useful to not add in
+ if isinstance(secret, unicode):
+ secret = secret.encode("utf-8")
+
+ #convert secret string into an integer
+ key_value = _crypt_secret_to_key(secret)
+ while len(secret) > 8:
+ secret = secret[8:]
+ key_value = mdes_encrypt_int_block(key_value, key_value, salt=0, rounds=1)
+ for i,c in enumerate(secret[:8]):
+ key_value ^= (ord(c)&0x7f)<<(57-8*i)
+
+ #run data through des using input of 0
+ result = mdes_encrypt_int_block(key_value, 0, salt=salt_value, rounds=rounds)
+
+ #run h64 encode on result
+ return h64.encode_int64(result)
+
+#=========================================================
+#algorithm information
+#=========================================================
+name = "ext-des-crypt"
+#stats: ??? bit checksum, ??? bit salt, ??? rounds, max ??? chars of secret
+
+setting_kwds = ("salt", "rounds")
+context_kwds = ()
+
+default_rounds = 1000
+min_rounds = 0
+max_rounds = 4095
+
+#=========================================================
+#internal helpers
+#=========================================================
+_pat = re.compile(r"""
+ ^
+ _
+ (?P<rounds>[./a-z0-9]{4})
+ (?P<salt>[./a-z0-9]{4})
+ (?P<chk>[./a-z0-9]{11})?
+ $""", re.X|re.I)
+
+def parse(hash):
+ if not hash:
+ raise ValueError, "no hash specified"
+ m = _pat.match(hash)
+ if not m:
+ raise ValueError, "invalid ext-des-crypt hash"
+ rounds, salt, chk = m.group("rounds", "salt", "chk")
+ return dict(
+ rounds=h64.decode_int24(rounds),
+ salt=salt,
+ checksum=chk,
+ )
+
+def render(rounds, salt, checksum=None):
+ if rounds < 0:
+ raise ValueError, "invalid rounds"
+ if len(salt) != 4:
+ raise ValueError, "invalid salt"
+ if checksum and len(checksum) != 11:
+ raise ValueError, "invalid checksum"
+ return "_%s%s%s" % (h64.encode_int24(rounds), salt, checksum or '')
+
+#=========================================================
+#primary interface
+#=========================================================
+def genconfig(salt=None, rounds=None):
+ """generate xxx configuration string
+
+ :param salt:
+ optional salt string to use.
+
+ if omitted, one will be automatically generated (recommended).
+
+ length must be 4 characters.
+ characters must be in range ``A-Za-z0-9./``.
+
+ :param rounds:
+
+ optional number of rounds, must be between 0 and 4095 inclusive.
+
+ :returns:
+ xxx configuration string.
+ """
+ salt = norm_salt(salt, 4, name=name)
+ rounds = norm_rounds(rounds, default_rounds, min_rounds, max_rounds, name=name)
+ return render(rounds, salt, None)
+
+def genhash(secret, config):
+ #parse and run through genconfig to validate configuration
+ #TODO: could *easily* optimize this to skip excess render/parse
+ info = parse(config)
+ info.pop("checksum")
+ config = genconfig(**info)
+ info = parse(config)
+ rounds, salt = info['rounds'], info['salt']
+
+ #run through chosen backend
+ checksum = raw_ext_crypt(secret, rounds, salt)
+ return render(rounds, salt, checksum)
+
+#=========================================================
+#secondary interface
+#=========================================================
+def encrypt(secret, **settings):
+ return genhash(secret, genconfig(**settings))
+
+def verify(secret, hash):
+ return hash == genhash(secret, hash)
+
+def identify(hash):
+ return bool(hash and _pat.match(hash))
+
+#=========================================================
+#eof
+#=========================================================
diff --git a/passlib/unix/md5_crypt.py b/passlib/hash/md5_crypt.py
index c46f4e9..d6260ef 100644
--- a/passlib/unix/md5_crypt.py
+++ b/passlib/hash/md5_crypt.py
@@ -1,28 +1,28 @@
-"""passlib - implementation of various password hashing functions
+"""passlib.hash.md5_crypt - md5-crypt algorithm
"""
#=========================================================
#imports
#=========================================================
-from __future__ import with_statement
#core
-import inspect
-import re
from hashlib import md5
+import re
import logging; log = logging.getLogger(__name__)
-import time
-import os
+from warnings import warn
#site
#libs
-from passlib.utils import h64_encode_3_offsets, h64_encode_1_offset
-from passlib.handler import ExtCryptHandler, register_crypt_handler
+from passlib.utils import norm_rounds, norm_salt, h64
#pkg
#local
__all__ = [
- 'Md5Crypt',
+ "genhash",
+ "genconfig",
+ "encrypt",
+ "identify",
+ "verify",
]
#=========================================================
-#default backend
+#pure-python backend
#=========================================================
def raw_md5_crypt(secret, salt, apr=False):
"perform raw md5 encryption"
@@ -32,9 +32,7 @@ def raw_md5_crypt(secret, salt, apr=False):
# implementation of $1$ wasn't sufficient.
#validate secret
- #FIXME: can't find definitive policy on how md5-crypt handles non-ascii.
- if isinstance(secret, unicode):
- secret = secret.encode("utf-8")
+ assert isinstance(secret, str) #should have been converted to unicode
#validate salt
if len(salt) > 8:
@@ -109,18 +107,18 @@ def raw_md5_crypt(secret, salt, apr=False):
#encode resulting hash
out = ''.join(
- h64_encode_3_offsets(result,
+ h64.encode_3_offsets(result,
idx+12 if idx < 4 else 5,
idx+6,
idx,
)
for idx in xrange(5)
- ) + h64_encode_1_offset(result, 11)
+ ) + h64.encode_1_offset(result, 11)
return out
#=========================================================
-#choose backend for md5-crypt
+#choose backend
#=========================================================
#fallback to default backend (defined above)
@@ -136,122 +134,102 @@ except ImportError:
crypt = None
else:
if crypt("test", "$1$test") == '$1$test$pi/xDtU5WFVRqYS6BMU8X/':
- backend = "stdlib"
+ backend = "os-crypt"
else:
crypt = None
#TODO: could check for libssl support (openssl passwd -1)
#=========================================================
-#id 1 -- md5
+#algorithm information
#=========================================================
-class Md5Crypt(ExtCryptHandler):
- """This provides the MD5-crypt algorithm, used in many 1990's era unix systems.
- It should be byte compatible with unix shadow hashes beginning with ``$1$``.
- """
- #=========================================================
- #crypt info
- #=========================================================
- name = 'md5-crypt'
- #stats: 96 bit checksum, 48 bit salt
-
- setting_kwds = ("salt",)
-
- salt_chars = 8
- min_salt_chars = 0
-
- #=========================================================
- #helpers
- #=========================================================
- _ident = "1"
-
- _pat = re.compile(r"""
- ^
- \$1
- \$(?P<salt>[A-Za-z0-9./]{,8})
- \$(?P<chk>[A-Za-z0-9./]{22})
- $
- """, re.X)
-
- @classmethod
- def parse(cls, hash):
- "parse an md5-crypt hash or config string"
- if not hash:
- raise ValueError, "no %s hash specified" % (cls.name,)
- m = cls._pat.match(hash)
- if not m:
- raise ValueError, "invalid %s hash" % (cls.name,)
- salt, chk = m.group("salt", "chk")
- return dict(
- salt=salt,
- checksum=chk,
- )
-
- @classmethod
- def render(cls, salt, checksum=None):
- "render md5-crypt hash or config string"
- return "$%s$%s$%s" % (cls._ident, salt, checksum or '')
-
- #=========================================================
- #1.4 frontend
- #=========================================================
- @classmethod
- def identify(cls, hash):
- "identify md5-crypt hash"
- return bool(hash and cls._pat.match(hash))
-
- @classmethod
- def genconfig(cls, salt=None):
- salt = cls._norm_salt(salt)
- return cls.render(salt)
-
- @classmethod
- def genhash(cls, secret, config):
- if crypt:
- #use OS's crypt(), should be faster than builtin backend
- config = cls._norm_config(config)
- if isinstance(secret, unicode):
- secret = secret.encode("utf-8")
- return crypt(secret, config)
- else:
- #fallback to builtin backend
- info = cls._parse_norm_config(config)
- checksum = raw_md5_crypt(secret, info['salt'])
- return cls.render(checksum=checksum, **info)
+name = "md5-crypt"
+#stats: 96 bit checksum, 48 bit salt
- #=========================================================
- #eoc
- #=========================================================
+setting_kwds = ("salt",)
+context_kwds = ()
-register_crypt_handler(Md5Crypt)
+#=========================================================
+#internal helpers
+#=========================================================
+_pat = re.compile(r"""
+ ^
+ \$1
+ \$(?P<salt>[A-Za-z0-9./]{,8})
+ (\$(?P<chk>[A-Za-z0-9./]{22})?)?
+ $
+ """, re.X)
+
+def parse(hash):
+ if not hash:
+ raise ValueError, "no hash specified"
+ m = _pat.match(hash)
+ if not m:
+ print hash
+ raise ValueError, "invalid md5-crypt hash"
+ salt, chk = m.group("salt", "chk")
+ return dict(
+ salt=salt,
+ checksum=chk,
+ )
+
+def render(salt, checksum=None):
+ return "$1$%s$%s" % (salt, checksum or '')
#=========================================================
-#apache variant of md5 crypt
+#primary interface
#=========================================================
-class AprMd5Crypt(Md5Crypt):
- "Apache variant of md5-crypt, used in htpasswd files"
+def genconfig(salt=None, rounds=None):
+ """generate md5-crypt configuration string
+
+ :param salt:
+ optional salt string to use.
- name = "apr-md5-crypt"
+ if omitted, one will be automatically generated (recommended).
- _ident = "apr1"
+ length must be between 0 and 8 characters inclusive.
+ characters must be in range ``A-Za-z0-9./``.
- _pat = re.compile(r"""
- ^
- \$apr1
- \$(?P<salt>[A-Za-z0-9./]{,8})
- \$(?P<chk>[A-Za-z0-9./]{22})
- $
- """, re.X)
+ :returns:
+ md5-crypt configuration string.
+ """
+ salt = norm_salt(salt, 0, 8, name=name)
+ return render(salt, None)
+
+def genhash(secret, config):
+ #parse and run through genconfig to validate configuration
+ info = parse(config)
+ info.pop("checksum")
+ config = genconfig(**info)
+
+ #FIXME: can't find definitive policy on how md5-crypt handles non-ascii.
+ if isinstance(secret, unicode):
+ secret = secret.encode("utf-8")
+
+ #run through chosen backend
+ if crypt:
+ #use OS's crypt(), should be faster than builtin backend
+ return crypt(secret, config)
+
+ else:
+ #fallback to builtin backend
+ info = parse(config)
+ salt = info['salt']
+ checksum = raw_md5_crypt(secret, salt)
+ return render(salt, checksum)
+
+#=========================================================
+#secondary interface
+#=========================================================
+def encrypt(secret, **settings):
+ return genhash(secret, genconfig(**settings))
- @classmethod
- def genhash(cls, secret, config):
- #TODO: could check for libssl support (openssl passwd -apr)
- info = cls._parse_norm_config(config)
- checksum = raw_md5_crypt(secret, info['salt'], apr=True)
- return cls.render(checksum=checksum, **info)
+def verify(secret, hash):
+ return hash == genhash(secret, hash)
-register_crypt_handler(AprMd5Crypt)
+def identify(hash):
+ return bool(hash and _pat.match(hash))
#=========================================================
-# eof
+#eof
#=========================================================
diff --git a/passlib/hash/mysql_323.py b/passlib/hash/mysql_323.py
new file mode 100644
index 0000000..4d01fac
--- /dev/null
+++ b/passlib/hash/mysql_323.py
@@ -0,0 +1,84 @@
+"""passlib.hash.mysql_323 - MySQL OLD_PASSWORD
+
+This implements Mysql's OLD_PASSWORD algorithm, introduced in version 3.2.3, deprecated in version 4.1.
+
+See :mod:`passlib.hash.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.
+"""
+#=========================================================
+#imports
+#=========================================================
+#core
+import re
+import logging; log = logging.getLogger(__name__)
+from warnings import warn
+#site
+#libs
+#pkg
+#local
+__all__ = [
+ "genhash",
+ "genconfig",
+ "encrypt",
+ "identify",
+ "verify",
+]
+
+#=========================================================
+#backend
+#=========================================================
+
+#=========================================================
+#algorithm information
+#=========================================================
+name = "mysql-323"
+#stats: 62 bit checksum, no salt
+
+setting_kwds = ()
+context_kwds = ()
+
+#=========================================================
+#internal helpers
+#=========================================================
+_pat = re.compile(r"^[0-9a-f]{16}$", re.I)
+
+#=========================================================
+#primary interface
+#=========================================================
+def genconfig():
+ return None
+
+def genhash(secret, config):
+ if config and not identify(config):
+ raise ValueError, "not a mysql-323 hash"
+
+ nr1 = 1345345333
+ nr2 = 0x12345671
+ add = 7
+ for c in secret:
+ if c in ' \t':
+ continue
+ tmp = ord(c)
+ nr1 ^= ((((nr1 & 63)+add)*tmp) + (nr1 << 8)) & 0xffffffff
+ nr2 = (nr2+((nr2 << 8) ^ nr1)) & 0xffffffff
+ add = (add+tmp) & 0xffffffff
+ return "%08x%08x" % (nr1 & 0x7fffffff, nr2 & 0x7fffffff)
+
+#=========================================================
+#secondary interface
+#=========================================================
+def encrypt(secret, **settings):
+ return genhash(secret, genconfig(**settings))
+
+def verify(secret, hash):
+ if not hash:
+ raise ValueError, "no hash specified"
+ return hash.lower() == genhash(secret, hash)
+
+def identify(hash):
+ return bool(hash and _pat.match(hash))
+
+#=========================================================
+#eof
+#=========================================================
diff --git a/passlib/hash/mysql_41.py b/passlib/hash/mysql_41.py
new file mode 100644
index 0000000..10b6bc2
--- /dev/null
+++ b/passlib/hash/mysql_41.py
@@ -0,0 +1,76 @@
+"""passlib.hash.mysql_41 - MySQL 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
+#libs
+#pkg
+#local
+__all__ = [
+ "genhash",
+ "genconfig",
+ "encrypt",
+ "identify",
+ "verify",
+]
+
+#=========================================================
+#backend
+#=========================================================
+
+#=========================================================
+#algorithm information
+#=========================================================
+name = "mysql-41"
+#stats: 160 bit checksum, no salt
+
+setting_kwds = ()
+context_kwds = ()
+
+#=========================================================
+#internal helpers
+#=========================================================
+_pat = re.compile(r"^\*[0-9A-F]{40}$", re.I)
+
+#=========================================================
+#primary interface
+#=========================================================
+def genconfig():
+ return None
+
+def genhash(secret, config):
+ if config and not identify(config):
+ raise ValueError, "not a mysql-41 hash"
+ return '*' + sha1(sha1(secret).digest()).hexdigest().upper()
+
+#=========================================================
+#secondary interface
+#=========================================================
+def encrypt(secret, **settings):
+ return genhash(secret, genconfig(**settings))
+
+def verify(secret, hash):
+ if not hash:
+ raise ValueError, "no hash specified"
+ return hash.upper() == genhash(secret, hash)
+
+def identify(hash):
+ return bool(hash and _pat.match(hash))
+
+#=========================================================
+#eof
+#=========================================================
diff --git a/passlib/hash/postgres_md5.py b/passlib/hash/postgres_md5.py
new file mode 100644
index 0000000..5a52573
--- /dev/null
+++ b/passlib/hash/postgres_md5.py
@@ -0,0 +1,91 @@
+"""passlib.hash.postgres_md5 - MD5-based algorithm used by Postgres for pg_shadow table
+
+This implements the md5-based hash algorithm used by Postgres to store
+passwords in the pg_shadow table.
+
+This algorithm shouldn't be used for any purpose besides Postgres interaction,
+it's a weak unsalted algorithm which could be attacked with a rainbow table
+built against common user names.
+
+.. warning::
+ This algorithm is slightly different from most of the others,
+ in that both encrypt() and verify() require you pass in
+ the name of the user account via the required 'user' keyword,
+ since postgres uses this in place of a salt :(
+
+Usage Example::
+
+ >>> from passlib.hash import postgres_md5 as pm
+ >>> pm.encrypt("mypass", user="postgres")
+ 'md55fba2ea04fd36069d2574ea71c8efe9d'
+ >>> pm.verify("mypass", 'md55fba2ea04fd36069d2574ea71c8efe9d', user="postgres")
+ True
+"""
+#=========================================================
+#imports
+#=========================================================
+#core
+from hashlib import md5
+import re
+import logging; log = logging.getLogger(__name__)
+from warnings import warn
+#site
+#libs
+#pkg
+#local
+__all__ = [
+ "genhash",
+ "genconfig",
+ "encrypt",
+ "identify",
+ "verify",
+]
+
+#=========================================================
+#backend
+#=========================================================
+
+#=========================================================
+#algorithm information
+#=========================================================
+name = "postgres-md5"
+#stats: 512 bit checksum, username used as salt
+
+setting_kwds = ()
+context_kwds = ("user",)
+
+#=========================================================
+#internal helpers
+#=========================================================
+_pat = re.compile(r"^md5[0-9a-f]{32}$")
+
+#=========================================================
+#primary interface
+#=========================================================
+def genconfig():
+ return None
+
+def genhash(secret, config, user):
+ if config and not identify(config):
+ raise ValueError, "not a postgres-md5 hash"
+ if not user:
+ raise ValueError, "user keyword must be specified for this algorithm"
+ return "md5" + md5(secret + user).hexdigest().lower()
+
+#=========================================================
+#secondary interface
+#=========================================================
+def encrypt(secret, user, **settings):
+ return genhash(secret, genconfig(**settings), user)
+
+def verify(secret, hash, user):
+ if not hash:
+ raise ValueError, "no hash specified"
+ return hash.lower() == genhash(secret, hash, user)
+
+def identify(hash):
+ return bool(hash and _pat.match(hash))
+
+#=========================================================
+#eof
+#=========================================================
diff --git a/passlib/hash/sha256_crypt.py b/passlib/hash/sha256_crypt.py
new file mode 100644
index 0000000..5116d41
--- /dev/null
+++ b/passlib/hash/sha256_crypt.py
@@ -0,0 +1,373 @@
+"""passlib.hash.sha256_crypt - SHA256-CRYPT
+
+This implementation is based on Ulrich Drepper's
+``sha-crypt specification <http://www.akkadia.org/drepper/sha-crypt.txt>``.
+It should be byte-compatible with unix shadow hashes beginning with ``$5$`` and ``$6%``.
+
+About
+=====
+This implementation is based on Ulrich Drepper's
+``sha-crypt specification <http://www.akkadia.org/drepper/sha-crypt.txt>``.
+It should be byte-compatible with unix shadow hashes beginning with ``$5$`` and ``$6%``.
+
+This module is not intended to be used directly,
+but merely as a backend for :mod:`passlib.unix.sha_crypt`
+when native sha crypt support is not available.
+
+Deviations from the Specification
+=================================
+
+Unicode
+-------
+The sha-crypt specification makes no statement regarding
+the unicode support, it merely takes in a series of bytes.
+
+In order to support non-ascii passwords and :class:`unicode` class,
+this implementation makes the arbitrary decision to encode all unicode passwords
+to ``utf-8`` before passing it into the encryption function.
+
+Salt Length
+-----------
+The sha-crypt specification allows salt strings of length 0-16 inclusive.
+However, most implementations (including this one) will only
+generate salts of length 16, though they allow the full range.
+
+Salt Characters
+---------------
+The charset used by salt strings is poorly defined for sha-crypt.
+
+The sha-crypt spec does not make any statements about the allowable
+salt charset, one way or the other. Furthermore, the reference implementation
+within the spec, and linux implementation, cheerfully allow
+all 8-bit values besides ``\x00`` and ``$``, and excluding
+those not by choice, but due to implementation details.
+Thus the argument could be made that all other characters should be allowed.
+
+However, allowing the characters ``:`` and ``\n`` would cause
+problems for the most common application of this algorithm,
+storage in ``/etc/shadow``. As well, the most unix shadow suites
+only generate salts using the chars ``./0-9A-Za-z``.
+
+Thus, as a compromise, this implementation of sha-crypt
+will allow all salt characters except for ``\x00\n:$``,
+in order to support as much of the specification as feasible;
+but it will only generate salts using the chars ``./0-9A-Za-z``,
+in order to remain compatible with the majority of hashes
+out there, in case other tools have made different assumptions.
+"""
+#=========================================================
+#imports
+#=========================================================
+#core
+from hashlib import sha256, sha512
+import re
+import logging; log = logging.getLogger(__name__)
+from warnings import warn
+#site
+#libs
+from passlib.utils import norm_rounds, norm_salt, h64
+#pkg
+#local
+__all__ = [
+ "genhash",
+ "genconfig",
+ "encrypt",
+ "identify",
+ "verify",
+]
+
+#=========================================================
+#pure-python backend (shared between sha256-crypt & sha512-crypt)
+#=========================================================
+def raw_sha_crypt(secret, salt, rounds, hash):
+ """perform raw sha crypt
+
+ :arg secret: password to encode (if unicode, encoded to utf-8)
+ :arg salt: salt string to use (required)
+ :arg rounds: int rounds
+ :arg hash: hash constructor function for 256/512 variant
+
+ :returns:
+ Returns tuple of ``(unencoded checksum, normalized salt, normalized rounds)``.
+
+ """
+ #validate secret
+ if isinstance(secret, unicode):
+ secret = secret.encode("utf-8")
+
+ #validate rounds
+ if rounds < 1000:
+ rounds = 1000
+ if rounds > 999999999:
+ rounds = 999999999
+
+ #validate salt
+ if any(c in salt for c in '\x00$'):
+ raise ValueError, "invalid chars in salt"
+ if len(salt) > 16:
+ salt = salt[:16]
+
+ #init helpers
+ def extend(source, size_ref):
+ "helper which repeats <source> digest string until it's the same length as <size_ref> string"
+ assert len(source) == chunk_size
+ size = len(size_ref)
+ return source * int(size/chunk_size) + source[:size % chunk_size]
+
+ #calc digest B
+ b = hash(secret)
+ chunk_size = b.digest_size #grab this once hash is created
+ b.update(salt)
+ a = b.copy() #make a copy to save a little time later
+ b.update(secret)
+ b_result = b.digest()
+ b_extend = extend(b_result, secret)
+
+ #begin digest A
+ #a = hash(secret) <- performed above
+ #a.update(salt) <- performed above
+ a.update(b_extend)
+
+ #for each bit in slen, add B or SECRET
+ value = len(secret)
+ while value > 0:
+ if value % 2:
+ a.update(b_result)
+ else:
+ a.update(secret)
+ value >>= 1
+
+ #finish A
+ a_result = a.digest()
+
+ #calc DP - hash of password, extended to size of password
+ dp = hash(secret * len(secret))
+ dp_result = extend(dp.digest(), secret)
+
+ #calc DS - hash of salt, extended to size of salt
+ ds = hash(salt * (16+ord(a_result[0])))
+ ds_result = extend(ds.digest(), salt) #aka 'S'
+
+ #
+ #calc digest C
+ #NOTE: this has been contorted a little to allow pre-computing
+ #some of the hashes. the original algorithm was that
+ #each round generates digest composed of:
+ # if round%2>0 => dp else lr
+ # if round%3>0 => ds
+ # if round%7>0 => dp
+ # if round%2>0 => lr else dp
+ #where lr is digest of the last round's hash (initially = a_result)
+ #
+
+ #pre-calculate some digests to speed up odd rounds
+ dp_hash = hash(dp_result).copy
+ dp_ds_hash = hash(dp_result + ds_result).copy
+ dp_dp_hash = hash(dp_result * 2).copy
+ dp_ds_dp_hash = hash(dp_result + ds_result + dp_result).copy
+
+ #pre-calculate some strings to speed up even rounds
+ ds_dp_result = ds_result + dp_result
+ dp_dp_result = dp_result * 2
+ ds_dp_dp_result = ds_result + dp_dp_result
+
+ #run through rounds
+ last_result = a_result
+ i = 0
+ while i < rounds:
+ if i % 2:
+ if i % 3:
+ if i % 7:
+ c = dp_ds_dp_hash()
+ else:
+ c = dp_ds_hash()
+ elif i % 7:
+ c = dp_dp_hash()
+ else:
+ c = dp_hash()
+ c.update(last_result)
+ else:
+ c = hash(last_result)
+ if i % 3:
+ if i % 7:
+ c.update(ds_dp_dp_result)
+ else:
+ c.update(ds_dp_result)
+ elif i % 7:
+ c.update(dp_dp_result)
+ else:
+ c.update(dp_result)
+ last_result = c.digest()
+ i += 1
+
+ #return unencoded result, along w/ normalized config values
+ return last_result, salt, rounds
+
+def raw_sha256_crypt(secret, salt, rounds):
+ "perform raw sha256-crypt; returns encoded checksum, normalized salt & rounds"
+ #run common crypt routine
+ result, salt, rounds = raw_sha_crypt(secret, salt, rounds, sha256)
+
+ #encode result
+ out = ''
+ a, b, c = 0, 10, 20
+ while a < 30:
+ out += h64.encode_3_offsets(result, c, b, a)
+ a, b, c = c+1, a+1, b+1
+ assert a == 30, "loop went to far: %r" % (a,)
+ out += h64.encode_2_offsets(result, 30, 31)
+ assert len(out) == 43, "wrong length: %r" % (out,)
+ return out, salt, rounds
+
+def raw_sha512_crypt(secret, salt, rounds):
+ "perform raw sha512-crypt; returns encoded checksum, normalized salt & rounds"
+ #run common crypt routine
+ result, salt, rounds = raw_sha_crypt(secret, salt, rounds, sha512)
+
+ #encode result
+ out = ''
+ a, b, c = 0, 21, 42
+ while c < 63:
+ out += h64.encode_3_offsets(result, c, b, a)
+ a, b, c = b+1, c+1, a+1
+ assert c == 63, "loop to far: %r" % (c,)
+ out += h64.encode_1_offset(result, 63)
+ assert len(out) == 86, "wrong length: %r" % (out,)
+ return out, salt, rounds
+
+#=========================================================
+#choose backend
+#=========================================================
+
+#fallback to default backend (defined above)
+backend = "builtin"
+
+#check if stdlib crypt is available, and if so, if OS supports $5$ and $6$
+#XXX: is this test expensive enough it should be delayed
+#until sha-crypt is requested?
+
+try:
+ from crypt import crypt
+except ImportError:
+ crypt = None
+else:
+ if crypt("test", "$5$rounds=1000$test") == "$5$rounds=1000$test$QmQADEXMG8POI5WDsaeho0P36yK3Tcrgboabng6bkb/":
+ backend = "os-crypt"
+ else:
+ crypt = None
+
+#=========================================================
+#algorithm information
+#=========================================================
+name = "sha256-crypt"
+#stats: 256 bit checksum, 96 bit salt, 1000..10e8-1 rounds
+
+setting_kwds = ("salt", "rounds")
+context_kwds = ()
+
+default_rounds = 40000 #current passlib default
+min_rounds = 1000
+max_rounds = 999999999
+
+#=========================================================
+#internal helpers
+#=========================================================
+_pat = re.compile(r"""
+ ^
+ \$5
+ (\$rounds=(?P<rounds>\d+))?
+ \$
+ (
+ (?P<salt1>[^:$]*)
+ |
+ (?P<salt2>[^:$]{0,16})
+ \$
+ (?P<chk>[A-Za-z0-9./]{43})?
+ )
+ $
+ """, re.X)
+
+def parse(hash):
+ if not hash:
+ raise ValueError, "no hash specified"
+ m = _pat.match(hash)
+ if not m:
+ raise ValueError, "invalid sha256-crypt hash"
+ rounds, salt1, salt2, chk = m.group("rounds", "salt1", "salt2", "chk")
+ return dict(
+ implicit_rounds = not rounds,
+ rounds=int(rounds) if rounds else 5000,
+ salt=salt1 or salt2,
+ checksum=chk,
+ )
+
+def render(rounds, salt, checksum=None, implicit_rounds=True):
+ assert '$' not in salt
+ if rounds == 5000 and implicit_rounds:
+ return "$5$%s$%s" % (salt, checksum or '')
+ else:
+ return "$5$rounds=%d$%s$%s" % (rounds, salt, checksum or '')
+
+#=========================================================
+#primary interface
+#=========================================================
+def genconfig(salt=None, rounds=None, implicit_rounds=True):
+ """generate sha256-crypt configuration string
+
+ :param salt:
+ optional salt string to use.
+
+ if omitted, one will be automatically generated (recommended).
+
+ length must be 0 .. 16 characters inclusive.
+ characters must be in range ``A-Za-z0-9./``.
+
+ :param rounds:
+
+ optional number of rounds, must be between 1000 and 999999999 inclusive.
+
+ :param implicit_rounds:
+
+ this is an internal option which generally doesn't need to be touched.
+
+ :returns:
+ sha256-crypt configuration string.
+ """
+ #TODO: allow salt charset 0-255 except for "\x00\n:$"
+ salt = norm_salt(salt, 0, 16, name=name)
+ rounds = norm_rounds(rounds, default_rounds, min_rounds, max_rounds, name=name)
+ return render(rounds, salt, None, implicit_rounds)
+
+def genhash(secret, config):
+ #parse and run through genconfig to validate configuration
+ info = parse(config)
+ info.pop("checksum")
+ config = genconfig(**info)
+
+ #run through chosen backend
+ if crypt:
+ #using system's crypt routine.
+ if isinstance(secret, unicode):
+ secret = secret.encode("utf-8")
+ return crypt(secret, config)
+ else:
+ #using builtin routine
+ info = parse(config)
+ checksum, salt, rounds = raw_sha256_crypt(secret, info['salt'], info['rounds'])
+ return render(rounds, salt, checksum, info['implicit_rounds'])
+
+#=========================================================
+#secondary interface
+#=========================================================
+def encrypt(secret, **settings):
+ return genhash(secret, genconfig(**settings))
+
+def verify(secret, hash):
+ return hash == genhash(secret, hash)
+
+def identify(hash):
+ return bool(hash and _pat.match(hash))
+
+#=========================================================
+#eof
+#=========================================================
diff --git a/passlib/hash/sha512_crypt.py b/passlib/hash/sha512_crypt.py
new file mode 100644
index 0000000..9c75641
--- /dev/null
+++ b/passlib/hash/sha512_crypt.py
@@ -0,0 +1,164 @@
+"""passlib.hash.sha512_crypt - SHA512-CRYPT
+
+This algorithm is identical to :mod:`sha256-crypt <passlib.hash.sha256_crypt>`,
+except that it uses SHA-512 instead of SHA-256. See that module
+for any handler specific details.
+"""
+#=========================================================
+#imports
+#=========================================================
+#core
+from hashlib import sha256
+import re
+import logging; log = logging.getLogger(__name__)
+from warnings import warn
+#site
+#libs
+from passlib.utils import norm_rounds, norm_salt, h64
+from passlib.hash.sha256_crypt import raw_sha512_crypt
+#pkg
+#local
+__all__ = [
+ "genhash",
+ "genconfig",
+ "encrypt",
+ "identify",
+ "verify",
+]
+
+#=========================================================
+#choose backend
+#=========================================================
+
+#fallback to default backend (defined above)
+backend = "builtin"
+
+#check if stdlib crypt is available, and if so, if OS supports $5$ and $6$
+#XXX: is this test expensive enough it should be delayed
+#until sha-crypt is requested?
+
+try:
+ from crypt import crypt
+except ImportError:
+ crypt = None
+else:
+ if crypt("test", "$6$rounds=1000$test") == "$6$rounds=1000$test$2M/Lx6MtobqjLjobw0Wmo4Q5OFx5nVLJvmgseatA6oMnyWeBdRDx4DU.1H3eGmse6pgsOgDisWBGI5c7TZauS0":
+ backend = "os-crypt"
+ else:
+ crypt = None
+
+#=========================================================
+#algorithm information
+#=========================================================
+name = "sha512-crypt"
+#stats: 512 bit checksum, 96 bit salt, 1000..10e8-1 rounds
+
+setting_kwds = ("salt", "rounds")
+context_kwds = ()
+
+default_rounds = 40000 #current passlib default
+min_rounds = 1000
+max_rounds = 999999999
+
+#=========================================================
+#internal helpers
+#=========================================================
+_pat = re.compile(r"""
+ ^
+ \$6
+ (\$rounds=(?P<rounds>\d+))?
+ \$
+ (
+ (?P<salt1>[^:$]*)
+ |
+ (?P<salt2>[^:$]{0,16})
+ \$
+ (?P<chk>[A-Za-z0-9./]{86})?
+ )
+ $
+ """, re.X)
+
+def parse(hash):
+ if not hash:
+ raise ValueError, "no hash specified"
+ m = _pat.match(hash)
+ if not m:
+ raise ValueError, "invalid sha512-crypt hash"
+ rounds, salt1, salt2, chk = m.group("rounds", "salt1", "salt2", "chk")
+ return dict(
+ implicit_rounds = not rounds,
+ rounds=int(rounds) if rounds else 5000,
+ salt=salt1 or salt2,
+ checksum=chk,
+ )
+
+def render(rounds, salt, checksum=None, implicit_rounds=True):
+ assert '$' not in salt
+ if rounds == 5000 and implicit_rounds:
+ return "$6$%s$%s" % (salt, checksum or '')
+ else:
+ return "$6$rounds=%d$%s$%s" % (rounds, salt, checksum or '')
+
+#=========================================================
+#primary interface
+#=========================================================
+def genconfig(salt=None, rounds=None, implicit_rounds=True):
+ """generate sha512-crypt configuration string
+
+ :param salt:
+ optional salt string to use.
+
+ if omitted, one will be automatically generated (recommended).
+
+ length must be 0 .. 16 characters inclusive.
+ characters must be in range ``A-Za-z0-9./``.
+
+ :param rounds:
+
+ optional number of rounds, must be between 1000 and 999999999 inclusive.
+
+ :param implicit_rounds:
+
+ this is an internal option which generally doesn't need to be touched.
+
+ :returns:
+ sha512-crypt configuration string.
+ """
+ #TODO: allow salt charset 0-255 except for "\x00\n:$"
+ salt = norm_salt(salt, 0, 16, name=name)
+ rounds = norm_rounds(rounds, default_rounds, min_rounds, max_rounds, name=name)
+ return render(rounds, salt, None, implicit_rounds)
+
+def genhash(secret, config):
+ #parse and run through genconfig to validate configuration
+ info = parse(config)
+ info.pop("checksum")
+ config = genconfig(**info)
+
+ #run through chosen backend
+ if crypt:
+ #using system's crypt routine.
+ if isinstance(secret, unicode):
+ secret = secret.encode("utf-8")
+ return crypt(secret, config)
+ else:
+ #using builtin routine
+ info = parse(config)
+ checksum, salt, rounds = raw_sha512_crypt(secret, info['salt'], info['rounds'])
+ return render(rounds, salt, checksum, info['implicit_rounds'])
+
+#=========================================================
+#secondary interface
+#=========================================================
+def encrypt(secret, **settings):
+ return genhash(secret, genconfig(**settings))
+
+def verify(secret, hash):
+ return hash == genhash(secret, hash)
+
+def identify(hash):
+ return bool(hash and _pat.match(hash))
+
+#=========================================================
+#eof
+#=========================================================
diff --git a/passlib/unix/sun_md5_crypt.py b/passlib/hash/sun_md5_crypt.py
index 6fba453..dba3034 100644
--- a/passlib/unix/sun_md5_crypt.py
+++ b/passlib/hash/sun_md5_crypt.py
@@ -1,4 +1,9 @@
-"""passlib - implementation of various password hashing functions
+"""passlib.hash.sun_md5_crypt - Sun's Md5 Crypt used on Solaris
+
+.. note::
+ Outside of being based on the md5 hash function,
+ this algorithm has almost nothing to do with the
+ bsd md5-crypt format.
.. note::
@@ -23,27 +28,30 @@
#=========================================================
#imports
#=========================================================
-from __future__ import with_statement
#core
-import inspect
-import re
from hashlib import md5
+import re
import logging; log = logging.getLogger(__name__)
-import time
-import os
+from warnings import warn
#site
#libs
-from passlib.utils import h64_encode_3_offsets, h64_encode_1_offset
-from passlib.handler import ExtCryptHandler, register_crypt_handler
+from passlib.utils import norm_rounds, norm_salt, h64
#pkg
#local
__all__ = [
- 'SunMd5Crypt',
+ "genhash",
+ "genconfig",
+ "encrypt",
+ "identify",
+ "verify",
]
#=========================================================
#backend
#=========================================================
+#=========================================================
+#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.
@@ -86,7 +94,7 @@ MAGIC_HAMLET = (
"Be all my sins remember'd.\n\x00" #<- apparently null at end of C string is included (test vector won't pass otherwise)
)
-def raw_sun_md5_crypt(secret, salt, rounds):
+def raw_sun_md5_crypt(secret, rounds, salt):
"given secret & salt, return encoded sun-md5-crypt checksum"
global MAGIC_HAMLET
@@ -117,8 +125,8 @@ def raw_sun_md5_crypt(secret, salt, rounds):
prefix = "$md5,rounds=%d$" % (rounds,)
else:
prefix = "$md5$"
- last_result = md5(secret + prefix + salt).digest()
- assert len(last_result) == 16
+ result = md5(secret + prefix + salt).digest()
+ assert len(result) == 16
#prepare constants for the per-round operations
ROUND_ITER = [
@@ -128,145 +136,161 @@ def raw_sun_md5_crypt(secret, salt, rounds):
cdata = MAGIC_HAMLET
#NOTE: many things have been inlined to speed up the loop
- # as much as possible. eg: the getbit routine - (last_bytes[bit//8] >> (bit%8)) & 1
+ # as much as possible. eg: the getbit routine - (rval[bit//8] >> (bit%8)) & 1
round = 0
while round < real_rounds:
#convert last result byte string to list of byte-ints for easy access
- last_bytes = [ ord(c) for c in last_result ]
- #XXX: could speed things up more by inlining last_bytes[xxx] w/ g=last_bytes.__getitem__ ... g(xxx)
+ rval = [ ord(c) for c in result ]
+ #XXX: could speed things up more by inlining rval[xxx] w/ g=rval.__getitem__ ... g(xxx)
#build up two 8-bit ints (x & y) to use as bit offsets for 'coin flip'
x = y = 0
for i,i3,i8,i11 in ROUND_ITER:
#use oa'th bit of last result as i'th bit of x
- bit = ((last_bytes[(last_bytes[i] >> (last_bytes[i3] % 5)) & 0x0f]) >> ((last_bytes[i3] >> (last_bytes[i] % 8)) & 1)) & 0x7F
- x |= ((last_bytes[bit//8] >> (bit%8)) & 1) << i
+ bit = ((rval[(rval[i] >> (rval[i3] % 5)) & 0x0f]) >> ((rval[i3] >> (rval[i] % 8)) & 1)) & 0x7F
+ x |= ((rval[bit//8] >> (bit%8)) & 1) << i
#use ob'th bit of last result as i'th bit of y
- bit = ((last_bytes[(last_bytes[i8] >> (last_bytes[i11] % 5)) & 0x0f]) >> ((last_bytes[i11] >> (last_bytes[i8] % 8)) & 1)) & 0x7F
- y |= ((last_bytes[bit//8] >> (bit%8)) & 1) << i
+ bit = ((rval[(rval[i8] >> (rval[i11] % 5)) & 0x0f]) >> ((rval[i11] >> (rval[i8] % 8)) & 1)) & 0x7F
+ y |= ((rval[bit//8] >> (bit%8)) & 1) << i
#based on round, pick high 7 bits or low 7 bits to use as actual offset
#(md5 digest contains exactly 128 bits)
- x = (x >> ((last_bytes[(round%128)//8] >> (round%8)) & 1)) & 0x7f
- y = (y >> ((last_bytes[((round+64)%128)//8] >> (round%8)) & 1)) & 0x7f
+ x = (x >> ((rval[(round%128)//8] >> (round%8)) & 1)) & 0x7f
+ y = (y >> ((rval[((round+64)%128)//8] >> (round%8)) & 1)) & 0x7f
#extract x'th and y'th bit, xoring them together to yeild "coin flip"
- coin = ((last_bytes[x//8] >> (x%8)) ^ (last_bytes[y//8] >> (y%8))) & 1
+ coin = ((rval[x//8] >> (x%8)) ^ (rval[y//8] >> (y%8))) & 1
#construct hash for this round
- h = md5(last_result)
+ h = md5(result)
if coin:
h.update(cdata)
h.update(str(round))
- last_result = h.digest()
+ result = h.digest()
round += 1
#encode output
#NOTE: appears to use same output encoding as md5-crypt
out = ''.join(
- h64_encode_3_offsets(last_result,
+ h64.encode_3_offsets(result,
idx+12 if idx < 4 else 5,
idx+6,
idx,
)
for idx in xrange(5)
- ) + h64_encode_1_offset(last_result, 11)
+ ) + h64.encode_1_offset(result, 11)
return out
#=========================================================
-#
+#algorithm information
#=========================================================
-class SunMd5Crypt(ExtCryptHandler):
- #=========================================================
- #crypt info
- #=========================================================
- name = 'sun-md5-crypt'
- #stats: 128 bit checksum, 48 bit salt, 0..2**32-4095 rounds
-
- setting_kwds = ("salt","rounds")
-
- salt_chars = 8
- min_salt_chars = 0
-
- default_rounds = 5000
- min_rounds = 0
- max_rounds = 4294963199 ##2**32-1-4096
-
- #=========================================================
- #helpers
- #=========================================================
- _pat = re.compile(r"""
- ^
- \$md5
- ([$,]rounds=(?P<rounds>\d+))?
- \$(?P<salt>[A-Za-z0-9./]{0,8})
- (\$(?P<chk>[A-Za-z0-9./]{22})?)?
- $
- """, re.X)
-
- #NOTE: trailing "$" is supposed to be part of config string,
- # supposed to take both, but render with "$"
- #NOTE: seen examples with both "," or "$" as md5/rounds separator,
- # not sure what official format is.
- # taking both, rendering ","
-
- @classmethod
- def parse(cls, hash):
- "parse a sun-md5-crypt hash or config string"
- if not hash:
- raise ValueError, "no sun-md5-crypt hash specified"
- m = cls._pat.match(hash)
- if not m:
- raise ValueError, "invalid sun-md5-crypt hash"
- salt, chk, rounds = m.group("salt", "chk", "rounds")
- #NOTE: this is *additional* rounds added to base 4096 specified by spec.
- #XXX: should we note whether "$" or "," was used as rounds separator?
- # not sure if that affects anything
- return dict(
- salt=salt,
- checksum=chk,
- rounds=int(rounds) if rounds else 0,
- )
+name = "sun-md5-crypt"
+#stats: 128 bit checksum, 48 bit salt, 0..2**32-4095 rounds
+
+setting_kwds = ("salt", "rounds")
+context_kwds = ()
+
+default_rounds = 5000 #current passlib default
+min_rounds = 0
+max_rounds = 4294963199 ##2**32-1-4096
+
+#=========================================================
+#internal helpers
+#=========================================================
+_pat = re.compile(r"""
+ ^
+ \$md5
+ ([$,]rounds=(?P<rounds>\d+))?
+ \$(?P<salt>[A-Za-z0-9./]{0,8})
+ (\$(?P<chk>[A-Za-z0-9./]{22})?)?
+ $
+ """, re.X)
+
+#NOTE: trailing "$" is supposed to be part of config string,
+# supposed to take both, but render with "$"
+#NOTE: seen examples with both "," or "$" as md5/rounds separator,
+# not sure what official format is.
+# taking both, rendering ","
+
+def parse(hash):
+ if not hash:
+ raise ValueError, "no hash specified"
+ m = _pat.match(hash)
+ if not m:
+ raise ValueError, "invalid sun-md5-crypt hash"
+ rounds, salt, chk = m.group("rounds", "salt", "chk")
+ #NOTE: this is *additional* rounds added to base 4096 specified by spec.
+ #XXX: should we note whether "$" or "," was used as rounds separator?
+ # not sure if that affects anything
+ return dict(
+ rounds=int(rounds) if rounds else 0,
+ salt=salt,
+ checksum=chk,
+ )
+
+def render(rounds, salt, checksum=None):
+ "render a sun-md5-crypt hash or config string"
+ if rounds > 0:
+ return "$md5,rounds=%d$%s$%s" % (rounds, salt, checksum or '')
+ else:
+ return "$md5$%s$%s" % (salt, checksum or '')
+
+#=========================================================
+#primary interface
+#=========================================================
+def genconfig(salt=None, rounds=None):
+ """generate xxx configuration string
+
+ :param salt:
+ optional salt string to use.
+
+ if omitted, one will be automatically generated (recommended).
+
+ length must be 0 to 8 characters inclusive.
+ characters must be in range ``A-Za-z0-9./``.
+
+ :param rounds:
+
+ optional number of rounds, must be between 0 and 4294963199 inclusive.
+
+ :returns:
+ sun-md5-crypt configuration string.
+ """
+ salt = norm_salt(salt, 0, 8, name=name)
+ rounds = norm_rounds(rounds, default_rounds, min_rounds, max_rounds, name=name)
+ return render(rounds, salt, None)
+
+def genhash(secret, config):
+ #parse and run through genconfig to validate configuration
+ #FIXME: could eliminate uneeded render/parse call
+ info = parse(config)
+ info.pop("checksum")
+ config = genconfig(**info)
+ info = parse(config)
+ rounds, salt = info['rounds'], info['salt']
+
+ #run through builtin backend
+ checksum = raw_sun_md5_crypt(secret, rounds, salt)
+ return render(rounds, salt, checksum)
+
+#=========================================================
+#secondary interface
+#=========================================================
+def encrypt(secret, **settings):
+ return genhash(secret, genconfig(**settings))
+
+def verify(secret, hash):
+ #normalize hash format so strings compare
+ if hash and hash.startswith("$md5$rounds="):
+ hash = "$md5,rounds=" + hash[12:]
+ return hash == genhash(secret, hash)
- @classmethod
- def render(cls, salt, rounds=0, checksum=None):
- "render a sun-md5-crypt hash or config string"
- if not checksum:
- checksum = ''
- if rounds:
- return "$md5,rounds=%d$%s$%s" % (rounds, salt, checksum)
- else:
- return "$md5$%s$%s" % (salt, checksum)
-
- #=========================================================
- #frontend
- #=========================================================
- @classmethod
- def identify(cls, hash):
- "identify sun-md5-crypt hash"
- return bool(hash and cls._pat.match(hash))
-
- @classmethod
- def genconfig(cls, salt=None, rounds=None):
- salt = cls._norm_salt(salt)
- rounds = cls._norm_rounds(rounds)
- return cls.render(salt, rounds)
-
- @classmethod
- def genhash(cls, secret, config):
- info = cls._parse_norm_config(config)
- checksum = raw_sun_md5_crypt(secret, info['salt'], info['rounds'])
- return cls.render(checksum=checksum, **info)
-
- #=========================================================
- #eoc
- #=========================================================
-
-register_crypt_handler(SunMd5Crypt)
+def identify(hash):
+ return bool(hash and _pat.match(hash))
#=========================================================
-# eof
+#eof
#=========================================================
diff --git a/passlib/lanman.py b/passlib/lanman.py
deleted file mode 100644
index 4ee93dc..0000000
--- a/passlib/lanman.py
+++ /dev/null
@@ -1,39 +0,0 @@
-"""
-
-the LMHASH and NTHASH algorithms are used in various windows related contexts,
-but generally not in a manner compatible with how passlib operates.
-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 only defines the lmhash() and nthash() functions for easy use.
-"""
-from binascii import hexlify
-from passlib.utils.des import des_encrypt_block
-from passlib.utils.md4 import md4
-
-LM_MAGIC = "KGS!@#$%"
-
-def lmhash(secret):
- #XXX: encoding should be oem ascii
- ns = secret.upper()[:14] + "\x00" * (14-len(secret))
- return hexlify(des_encrypt_block(ns[:7], LM_MAGIC) + des_encrypt_block(ns[7:], LM_MAGIC))
-
-def nthash(secret):
- return md4(secret.encode("utf-16le")).hexdigest()
-
-###hashes from http://msdn.microsoft.com/en-us/library/cc245828(v=prot.10).aspx
-### among other places
-##for secret, hash in [
-## ("OLDPASSWORD", "c9b81d939d6fd80cd408e6b105741864"),
-## ("NEWPASSWORD", '09eeab5aa415d6e4d408e6b105741864'),
-## ("welcome", "c23413a8a1e7665faad3b435b51404ee"),
-## ]:
-##
-## print secret, lmhash(secret), hash == lmhash(secret)
-
-##for secret, hash in [
-## ("OLDPASSWORD", "6677b2c394311355b54f25eec5bfacf5"),
-## ("NEWPASSWORD", "256781a62031289d3c2c98c14f1efc8c"),
-## ]:
-## print secret, lmhash(secret), hash == nthash(secret)
diff --git a/passlib/mysql.py b/passlib/mysql.py
index aa35d49..82eaa5e 100644
--- a/passlib/mysql.py
+++ b/passlib/mysql.py
@@ -2,127 +2,29 @@
#=========================================================
#imports
#=========================================================
-from __future__ import with_statement
#core
-import inspect
-import re
-import hashlib
import logging; log = logging.getLogger(__name__)
-import time
-import os
#site
#libs
+from passlib.hash import mysql_323, mysql_41
from passlib.context import CryptContext
-from passlib.handler import CryptHandler, register_crypt_handler
#pkg
#local
__all__ = [
- 'Mysql10Crypt',
- 'Mysql41Crypt',
+ #helpful imports of handlers
+ 'mysql_10',
+ 'mysql_41',
+ #contexts
'mysql10_context',
'mysql_context',
]
#=========================================================
-#sql database hashes
-#=========================================================
-class Mysql10Crypt(CryptHandler):
- """This implements Mysql's OLD_PASSWORD algorithm, used prior to version 4.1.
-
- See :class:`Mysql41Crypt` for the new algorithm was put in place in version 4.1
-
- This function is known to be very insecure,
- and should only be used to verify existing password hashes.
-
- """
- #=========================================================
- #crypt information
- #=========================================================
- name = "mysql-10"
-
- #stats: 256 bit checksum, no salt
-
- #=========================================================
- #frontend
- #=========================================================
- _pat = re.compile(r"^[0-9a-f]{16}$", re.I)
-
- @classmethod
- def identify(cls, hash):
- return bool(hash and cls._pat.match(hash))
-
- @classmethod
- def genhash(cls, secret, config):
- nr1 = 1345345333
- nr2 = 0x12345671
- add = 7
- for c in secret:
- if c in ' \t':
- continue
- tmp = ord(c)
- nr1 ^= ((((nr1 & 63)+add)*tmp) + (nr1 << 8)) & 0xffffffff
- nr2 = (nr2+((nr2 << 8) ^ nr1)) & 0xffffffff
- add = (add+tmp) & 0xffffffff
- return "%08x%08x" % (nr1 & 0x7fffffff, nr2 & 0x7fffffff)
-
- @classmethod
- def verify(cls, secret, hash):
- if not cls.identify(hash):
- raise ValueError, "not a mysql-10 hash"
- return hash.lower() == cls.genhash(secret, None)
-
- #=========================================================
- #eoc
- #=========================================================
-register_crypt_handler(Mysql10Crypt)
-
-class Mysql41Crypt(CryptHandler):
- """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
- """
- #=========================================================
- #crypt information
- #=========================================================
- name = "mysql-41"
-
- #stats: 160 bit checksum, no salt
-
- #=========================================================
- #frontend
- #=========================================================
- _pat = re.compile(r"^\*[0-9A-F]{40}$", re.I)
-
- @classmethod
- def identify(cls, hash):
- return bool(hash and cls._pat.match(hash))
-
- @classmethod
- def genhash(cls, secret, config):
- return '*' + hashlib.sha1(hashlib.sha1(secret).digest()).hexdigest().upper()
-
- @classmethod
- def verify(cls, secret, hash):
- if not cls.identify(hash):
- raise ValueError, "not a mysql-41 hash"
- return hash.upper() == cls.genhash(secret, None)
-
- #=========================================================
- #eoc
- #=========================================================
-
-register_crypt_handler(Mysql41Crypt)
-
-#=========================================================
#some db context helpers
#=========================================================
-mysql10_context = CryptContext([Mysql10Crypt])
-mysql_context = CryptContext([Mysql10Crypt, Mysql41Crypt])
+mysql3_context = CryptContext([mysql_323])
+mysql4_context = CryptContext([mysql_323, mysql_41])
#=========================================================
# eof
diff --git a/passlib/postgres.py b/passlib/postgres.py
index 4a456d8..f726819 100644
--- a/passlib/postgres.py
+++ b/passlib/postgres.py
@@ -12,72 +12,19 @@ import time
import os
#site
#libs
+from passlib.hash import postgres_md5
from passlib.context import CryptContext
-from passlib.handler import CryptHandler, register_crypt_handler
#pkg
#local
__all__ = [
- 'PostgresMd5Crypt',
+ 'postgres_md5',
+ 'postgres_context',
]
-#=========================================================
-#sql database hashes
-#=========================================================
-class PostgresMd5Crypt(CryptHandler):
- """This implements the md5-based hash algorithm used by Postgres to store
- passwords in the pg_shadow table.
-
- This algorithm shouldn't be used for any purpose besides Postgres interaction,
- it's a weak unsalted algorithm which could easily be attacked with a rainbow table.
-
- .. warning::
- This algorithm is slightly different from most of the others,
- in that both encrypt() and verify() require you pass in
- the name of the user account via the required 'user' keyword,
- since postgres uses this in place of a salt :(
-
- Usage Example::
-
- >>> from passlib import hash
- >>> crypt = hash.PostgresMd5Crypt()
- >>> crypt.encrypt("mypass", user="postgres")
- 'md55fba2ea04fd36069d2574ea71c8efe9d'
- >>> crypt.verify("mypass", 'md55fba2ea04fd36069d2574ea71c8efe9d', user="postgres")
- True
- """
- #=========================================================
- #crypt information
- #=========================================================
- name = "postgres-md5"
-
- context_kwds = ("user",)
-
- #stats: 512 bit checksum, username used as salt
-
- #=========================================================
- #frontend
- #=========================================================
- _pat = re.compile(r"^md5[0-9a-f]{32}$")
-
- @classmethod
- def identify(cls, hash):
- return bool(hash and cls._pat.match(hash))
-
- @classmethod
- def genhash(cls, secret, config, user):
- if not user:
- raise ValueError, "user keyword must be specified for this algorithm"
- return "md5" + hashlib.md5(secret + user).hexdigest().lower()
-
- #=========================================================
- #eoc
- #=========================================================
-
-register_crypt_handler(PostgresMd5Crypt)
#=========================================================
#db contexts
#=========================================================
-postgres_context = CryptContext([PostgresMd5Crypt])
+postgres_context = CryptContext([postgres_md5])
#=========================================================
# eof
diff --git a/passlib/tests/handler_utils.py b/passlib/tests/handler_utils.py
index fb92c4e..b6fd4ac 100644
--- a/passlib/tests/handler_utils.py
+++ b/passlib/tests/handler_utils.py
@@ -5,6 +5,7 @@
#core
import re
#site
+from nose.plugins.skip import SkipTest
#pkg
from passlib.tests.utils import TestCase
#module
@@ -73,7 +74,11 @@ class _HandlerTestCase(TestCase):
#useful when multiple handler test classes being run.
#default behavior should be sufficient
def case_prefix(self):
- return self.handler.name if self.handler else self.__class__.__name__
+ name = self.handler.name if self.handler else self.__class__.__name__
+ backend = getattr(self.handler, "backend", None) #set by some of the builtin handlers
+ if backend:
+ name += " (%s backend)" % (backend,)
+ return name
#=========================================================
#alg interface helpers - allows subclass to overide how
@@ -109,7 +114,7 @@ class _HandlerTestCase(TestCase):
name = ga("name")
self.assert_(name, "name not defined:")
self.assert_(name.lower() == name, "name not lower-case:")
- self.assert_(re.match("^[a-z0-9-]+$", name), "name must be alphanum + hyphen:")
+ self.assert_(re.match("^[a-z0-9-]+$", name), "name must be alphanum + hyphen: %r" % (name,))
#=========================================================
#identify
@@ -220,7 +225,7 @@ class _HandlerTestCase(TestCase):
def test_33_encrypt_gensalt(self):
"test encrypt() generates new salt each time"
if 'salt' not in self.handler.setting_kwds:
- return
+ raise SkipTest
for secret, hash in self.known_correct:
hash2 = self.do_encrypt(secret)
self.assertNotEqual(hash, hash2)
@@ -276,8 +281,12 @@ class _HandlerTestCase(TestCase):
#
#=========================================================
+ #TODO: check genhash works
+ #TODO: check genconfig works
+
#TODO: check parse method works
#TODO: check render method works
+ #TODO: check default/min/max_rounds valid if present
#=========================================================
#eoc
diff --git a/passlib/tests/test_context.py b/passlib/tests/test_context.py
index 481cee3..0a97213 100644
--- a/passlib/tests/test_context.py
+++ b/passlib/tests/test_context.py
@@ -12,7 +12,7 @@ from passlib.context import CryptContext
from passlib.tests.utils import TestCase
##from passlib.unix.des_crypt import DesCrypt
##from passlib.unix.sha_crypt import Sha512Crypt
-from passlib.unix.md5_crypt import Md5Crypt as AnotherHash
+import passlib.hash.md5_crypt as AnotherHash
from passlib.tests.test_handler import UnsaltedHash, SaltedHash
#module
log = getLogger(__name__)
diff --git a/passlib/tests/test_des_crypt.py b/passlib/tests/test_des_crypt.py
deleted file mode 100644
index b0b6965..0000000
--- a/passlib/tests/test_des_crypt.py
+++ /dev/null
@@ -1,190 +0,0 @@
-"""tests for passlib.pwhash -- (c) Assurance Technologies 2003-2009"""
-#=========================================================
-#imports
-#=========================================================
-from __future__ import with_statement
-#core
-import hashlib
-from logging import getLogger
-#site
-#pkg
-from passlib.tests.utils import TestCase, enable_test
-from passlib.tests.handler_utils import _HandlerTestCase
-from passlib.utils._slow_des_crypt import crypt as builtin_crypt
-import passlib.unix.des_crypt as mod
-#module
-log = getLogger(__name__)
-
-#=========================================================
-#test frontend class
-#=========================================================
-class DesCryptTest(_HandlerTestCase):
- "test DesCrypt algorithm"
- handler = mod.DesCrypt
- secret_chars = 8
-
- known_correct = (
- #secret, example hash which matches secret
- ('', 'OgAwTx2l6NADI'),
- (' ', '/Hk.VPuwQTXbc'),
- ('test', 'N1tQbOFcM5fpg'),
- ('Compl3X AlphaNu3meric', 'um.Wguz3eVCx2'),
- ('4lpHa N|_|M3r1K W/ Cur5Es: #$%(*)(*%#', 'sNYqfOyauIyic'),
- ('AlOtBsOl', 'cEpWz5IUCShqM'),
- (u'hell\u00D6', 'saykDgk3BPZ9E'),
- )
- known_invalid = (
- #bad char in otherwise correctly formatted hash
- '!gAwTx2l6NADI',
- )
-
-class ExtDesCryptTest(_HandlerTestCase):
- "test ExtDesCrypt algorithm"
- handler = mod.ExtDesCrypt
- known_correct = (
- (" ", "_K1..crsmZxOLzfJH8iw"),
- ("my", "_K1..crsmjChSwFUvdpw"),
- ("my socra", "_K1..crsmf/9NzZr1fLM"),
- ("my socrates", '_K1..crsmOv1rbde9A9o'),
- ("my socrates note", "_K1..crsm/2qeAhdISMA"),
- )
- known_invalid = (
- #bad char in otherwise correctly formatted hash
- "_K1.!crsmZxOLzfJH8iw"
- )
-
-#=========================================================
-#test activate backend (stored in mod._crypt)
-#=========================================================
-class _DesCryptBackendTest(TestCase):
- "test builtin unix crypt backend"
-
- def get_crypt(self):
- raise NotImplementedError
-
- known_correct = DesCryptTest.known_correct
-
- def test_knowns(self):
- "test known crypt results"
- crypt = self.get_crypt()
- for secret, result in self.known_correct:
-
- #make sure crypt verifies preserving just salt
- out = crypt(secret, result[:2])
- self.assertEqual(out, result, "secret=%r using salt alone:" % (secret,))
-
- #make sure crypt verifies preseving salt + fragment of known hash
- out = crypt(secret, result[:6])
- self.assertEqual(out, result, "secret=%r using salt + fragment:" % (secret,))
-
- #make sure crypt verifies using whole known hash
- out = crypt(secret, result)
- self.assertEqual(out, result, "secret=%r using whole hash:" % (secret,))
-
- #TODO: deal with border cases where host crypt & bps crypt differ
- # (none of which should impact the normal use cases)
- #border cases:
- # no salt given, empty salt given, 1 char salt
- # salt w/ non-b64 chars (linux crypt handles this _somehow_)
- #test that \x00 is NOT allowed
- #test that other chars _are_ allowed
-
- def test_null_in_key(self):
- "test null chars in secret"
- crypt = self.get_crypt()
- #NOTE: this is done to match stdlib crypt behavior.
- # would raise ValueError if otherwise had free choice
- self.assertRaises(ValueError, crypt, "hello\x00world", "ab")
-
- def test_invalid_salt(self):
- "test invalid salts"
- crypt = self.get_crypt()
-
- #NOTE: stdlib crypt's behavior is to return "" in this case.
- # passlib wraps stdlib crypt so it raises ValueError
- self.assertRaises(ValueError, crypt, "fooey","")
-
- #NOTE: stdlib crypt's behavior is rather bizarre in this case
- # (see wrapper in passlib.unix_crypt).
- # passlib wraps stdlib crypt so it raises ValueError
- self.assertRaises(ValueError, crypt, "fooey","f")
-
- #FIXME: stdlib crypt does something unpredictable
- #if passed salt chars outside of H64.CHARS range.
- #not sure *what* it's algorithm is. should figure that out.
- # until then, passlib wraps stdlib crypt so this causes ValueError
- self.assertRaises(ValueError, crypt, "fooey", "a@")
-
-if mod.backend != "builtin" and enable_test("fallback-backend"):
- class BuiltinDesCryptBackendTest(_DesCryptBackendTest):
- "test builtin des-crypt backend"
- case_prefix = "builtin des-crypt() backend"
-
- def get_crypt(self):
- return builtin_crypt
-
-if enable_test("backends"):
- #NOTE: this will generally be the stdlib implementation,
- #which of course is correct, so doing this more to detect deviations in builtin implementation
- class ActiveDesCryptBackendTest(_DesCryptBackendTest):
- "test active des-crypt backend"
- case_prefix = mod.backend + " des-crypt() backend"
-
- def get_crypt(self):
- return mod.crypt
-
-
-class DesTest(TestCase):
-
- #test vectors taken from http://www.skepticfiles.org/faq/testdes.htm
-
- #(key, plaintext, ciphertext) all as 64 bit
- test_des_vectors = [
- (int(line[4:21],16), int(line[21:38],16), int(line[38:],16))
- for line in
- """ 0000000000000000 0000000000000000 8CA64DE9C1B123A7
- FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF 7359B2163E4EDC58
- 3000000000000000 1000000000000001 958E6E627A05557B
- 1111111111111111 1111111111111111 F40379AB9E0EC533
- 0123456789ABCDEF 1111111111111111 17668DFC7292532D
- 1111111111111111 0123456789ABCDEF 8A5AE1F81AB8F2DD
- 0000000000000000 0000000000000000 8CA64DE9C1B123A7
- FEDCBA9876543210 0123456789ABCDEF ED39D950FA74BCC4
- 7CA110454A1A6E57 01A1D6D039776742 690F5B0D9A26939B
- 0131D9619DC1376E 5CD54CA83DEF57DA 7A389D10354BD271
- 07A1133E4A0B2686 0248D43806F67172 868EBB51CAB4599A
- 3849674C2602319E 51454B582DDF440A 7178876E01F19B2A
- 04B915BA43FEB5B6 42FD443059577FA2 AF37FB421F8C4095
- 0113B970FD34F2CE 059B5E0851CF143A 86A560F10EC6D85B
- 0170F175468FB5E6 0756D8E0774761D2 0CD3DA020021DC09
- 43297FAD38E373FE 762514B829BF486A EA676B2CB7DB2B7A
- 07A7137045DA2A16 3BDD119049372802 DFD64A815CAF1A0F
- 04689104C2FD3B2F 26955F6835AF609A 5C513C9C4886C088
- 37D06BB516CB7546 164D5E404F275232 0A2AEEAE3FF4AB77
- 1F08260D1AC2465E 6B056E18759F5CCA EF1BF03E5DFA575A
- 584023641ABA6176 004BD6EF09176062 88BF0DB6D70DEE56
- 025816164629B007 480D39006EE762F2 A1F9915541020B56
- 49793EBC79B3258F 437540C8698F3CFA 6FBF1CAFCFFD0556
- 4FB05E1515AB73A7 072D43A077075292 2F22E49BAB7CA1AC
- 49E95D6D4CA229BF 02FE55778117F12A 5A6B612CC26CCE4A
- 018310DC409B26D6 1D9D5C5018F728C2 5F4C038ED12B2E41
- 1C587F1C13924FEF 305532286D6F295A 63FAC0D034D9F793
- 0101010101010101 0123456789ABCDEF 617B3A0CE8F07100
- 1F1F1F1F0E0E0E0E 0123456789ABCDEF DB958605F8C8C606
- E0FEE0FEF1FEF1FE 0123456789ABCDEF EDBFD1C66C29CCC7
- 0000000000000000 FFFFFFFFFFFFFFFF 355550B2150E2451
- FFFFFFFFFFFFFFFF 0000000000000000 CAAAAF4DEAF1DBAE
- 0123456789ABCDEF 0000000000000000 D5D44FF720683D0D
- FEDCBA9876543210 FFFFFFFFFFFFFFFF 2A2BB008DF97C2F2
- """.split("\n") if line.strip()
- ]
-
- def test_des_encrypt_int_block(self):
- from passlib.utils._slow_des_crypt import des_encrypt_int_block
- for k,p,c in self.test_des_vectors:
- result = des_encrypt_int_block(k,p)
- self.assertEqual(result, c, "key=%r p=%r:" % (k,p))
-
-#=========================================================
-#EOF
-#=========================================================
diff --git a/passlib/tests/test_frontend.py b/passlib/tests/test_frontend.py
index 87379ef..3182068 100644
--- a/passlib/tests/test_frontend.py
+++ b/passlib/tests/test_frontend.py
@@ -20,10 +20,10 @@ def get_crypt_cases():
#this test suite uses info stored in the specific hash algs' test suites,
#so we have to import them here.
- from passlib.tests.test_sha_crypt import Sha256CryptTest, Sha512CryptTest
- from passlib.tests.test_des_crypt import DesCryptTest
- from passlib.tests.test_bcrypt import BCryptTest
- from passlib.tests.test_md5_crypt import Md5CryptTest
+ from passlib.tests.test_hash_sha_crypt import Sha256CryptTest, Sha512CryptTest
+ from passlib.tests.test_hash_des_crypt import DesCryptTest
+ from passlib.tests.test_hash_bcrypt import BCryptTest
+ from passlib.tests.test_hash_md5_crypt import Md5CryptTest
crypt_cases = [ DesCryptTest, Md5CryptTest, Sha256CryptTest]
if BCryptTest:
diff --git a/passlib/tests/test_handler.py b/passlib/tests/test_handler.py
index 8668c3d..718c22f 100644
--- a/passlib/tests/test_handler.py
+++ b/passlib/tests/test_handler.py
@@ -11,17 +11,17 @@ from logging import getLogger
#pkg
from passlib.handler import CryptHandler
from passlib.tests.handler_utils import _HandlerTestCase
-from passlib.utils import generate_h64_salt
+from passlib.utils import gen_salt
#module
log = getLogger(__name__)
#=========================================================
#sample algorithms - these serve as known quantities
# to test the unittests themselves, as well as other
-# parts of passlib
+# parts of passlib. they shouldn't be used as actual password schemes.
#=========================================================
class UnsaltedHash(CryptHandler):
- "example algorithm which lacks a salt [REALLY INSECURE - DO NOT USE]"
+ "example algorithm which lacks a salt"
name = "unsalted-example"
#stats: 160 bit checksum, no salt
@@ -34,7 +34,7 @@ class UnsaltedHash(CryptHandler):
return hashlib.sha1("boblious" + secret).hexdigest()
class SaltedHash(CryptHandler):
- "example algorithm with a salt [REALLY INSECURE - DO NOT USE]"
+ "example algorithm with a salt"
name = "salted-example"
#stats: 160 bit checksum, 12 bit salt
@@ -45,7 +45,7 @@ class SaltedHash(CryptHandler):
return bool(hash and re.match("^@salt[0-9a-zA-Z./]{2}[0-9a-f]{40}$", hash))
@classmethod
- def _parse(cls, hash):
+ def parse(cls, hash):
if not cls.identify(hash):
raise ValueError, "not a salted-example hash"
return dict(
@@ -54,7 +54,7 @@ class SaltedHash(CryptHandler):
)
@classmethod
- def _render(cls, salt, checksum):
+ def render(cls, salt, checksum):
assert len(salt) == 2
assert len(checksum) == 40
return "@salt%s%s" % (salt, checksum)
@@ -62,17 +62,17 @@ class SaltedHash(CryptHandler):
@classmethod
def genconfig(cls, salt=None):
if not salt:
- salt = generate_h64_salt(2)
- return cls._render(salt[:2], '0' * 40)
+ salt = gen_salt(2)
+ return cls.render(salt[:2], '0' * 40)
@classmethod
def genhash(cls, secret, config):
- salt = cls._parse(config)['salt']
- checksum = hashlib.sha1(salt+secret).hexdigest()
- return cls._render(salt, checksum)
+ salt = cls.parse(config)['salt']
+ checksum = hashlib.sha1(salt + secret + salt).hexdigest()
+ return cls.render(salt, checksum)
#=========================================================
-#test sample algorithms
+#test sample algorithms - really a self-test of _HandlerTestCase
#=========================================================
#TODO: provide data samples for algorithms
diff --git a/passlib/tests/test_bcrypt.py b/passlib/tests/test_hash_bcrypt.py
index 78abea2..239afb6 100755
--- a/passlib/tests/test_bcrypt.py
+++ b/passlib/tests/test_hash_bcrypt.py
@@ -25,7 +25,7 @@ try:
except ImportError:
pybcrypt = None
#pkg
-from passlib.tests.utils import TestCase, enable_test
+from passlib.tests.utils import TestCase, enable_option
from passlib.utils import _slow_bcrypt as slow_bcrypt
from passlib.tests.handler_utils import _HandlerTestCase
import passlib.unix.bcrypt as mod
@@ -200,13 +200,13 @@ class _BCryptTestBase(TestCase):
#eoc
#=========================================================
-if enable_test("slow") and enable_test("fallback-backends" if pybcrypt else "backends"):
+if enable_option("slow") and enable_option("all-backends" if pybcrypt else "backends"):
class SlowBcryptTest(_BCryptTestBase):
"test slow bcrypt module"
case_prefix = "builtin bcrypt() backend"
mod = slow_bcrypt
-if pybcrypt and enable_test("backends"):
+if pybcrypt and enable_option("backends"):
#if pybcrypt is installed, run our unitest on them too,
#just to ensure slow_bcrypt's interface is compatible.
class PyBcryptTest(_BCryptTestBase):
diff --git a/passlib/tests/test_hash_des_crypt.py b/passlib/tests/test_hash_des_crypt.py
new file mode 100644
index 0000000..fabe2e8
--- /dev/null
+++ b/passlib/tests/test_hash_des_crypt.py
@@ -0,0 +1,139 @@
+"""tests for passlib.pwhash -- (c) Assurance Technologies 2003-2009"""
+#=========================================================
+#imports
+#=========================================================
+from __future__ import with_statement
+#core
+import hashlib
+from logging import getLogger
+#site
+#pkg
+from passlib.tests.utils import TestCase, enable_option
+from passlib.tests.handler_utils import _HandlerTestCase
+import passlib.hash.des_crypt as mod
+import passlib.hash.ext_des_crypt as mod2
+#module
+log = getLogger(__name__)
+
+#=========================================================
+#test frontend class
+#=========================================================
+class DesCryptTest(_HandlerTestCase):
+ "test DesCrypt algorithm"
+ handler = mod
+ secret_chars = 8
+
+ known_correct = (
+ #secret, example hash which matches secret
+ ('', 'OgAwTx2l6NADI'),
+ (' ', '/Hk.VPuwQTXbc'),
+ ('test', 'N1tQbOFcM5fpg'),
+ ('Compl3X AlphaNu3meric', 'um.Wguz3eVCx2'),
+ ('4lpHa N|_|M3r1K W/ Cur5Es: #$%(*)(*%#', 'sNYqfOyauIyic'),
+ ('AlOtBsOl', 'cEpWz5IUCShqM'),
+ (u'hell\u00D6', 'saykDgk3BPZ9E'),
+ )
+ known_invalid = (
+ #bad char in otherwise correctly formatted hash
+ '!gAwTx2l6NADI',
+ )
+
+class ExtDesCryptTest(_HandlerTestCase):
+ "test ExtDesCrypt algorithm"
+ handler = mod2
+ known_correct = (
+ (" ", "_K1..crsmZxOLzfJH8iw"),
+ ("my", "_K1..crsmjChSwFUvdpw"),
+ ("my socra", "_K1..crsmf/9NzZr1fLM"),
+ ("my socrates", '_K1..crsmOv1rbde9A9o'),
+ ("my socrates note", "_K1..crsm/2qeAhdISMA"),
+ )
+ known_invalid = (
+ #bad char in otherwise correctly formatted hash
+ "_K1.!crsmZxOLzfJH8iw"
+ )
+
+#=========================================================
+#test activate backend (stored in mod._crypt)
+#=========================================================
+#TODO: make these tests work again
+##class _DesCryptBackendTest(TestCase):
+## "test builtin unix crypt backend"
+##
+## def get_crypt(self):
+## raise NotImplementedError
+##
+## known_correct = DesCryptTest.known_correct
+##
+## def test_knowns(self):
+## "test known crypt results"
+## crypt = self.get_crypt()
+## for secret, result in self.known_correct:
+##
+## #make sure crypt verifies preserving just salt
+## out = crypt(secret, result[:2])
+## self.assertEqual(out, result, "secret=%r using salt alone:" % (secret,))
+##
+## #make sure crypt verifies preseving salt + fragment of known hash
+## out = crypt(secret, result[:6])
+## self.assertEqual(out, result, "secret=%r using salt + fragment:" % (secret,))
+##
+## #make sure crypt verifies using whole known hash
+## out = crypt(secret, result)
+## self.assertEqual(out, result, "secret=%r using whole hash:" % (secret,))
+##
+## #TODO: deal with border cases where host crypt & bps crypt differ
+## # (none of which should impact the normal use cases)
+## #border cases:
+## # no salt given, empty salt given, 1 char salt
+## # salt w/ non-b64 chars (linux crypt handles this _somehow_)
+## #test that \x00 is NOT allowed
+## #test that other chars _are_ allowed
+##
+## def test_null_in_key(self):
+## "test null chars in secret"
+## crypt = self.get_crypt()
+## #NOTE: this is done to match stdlib crypt behavior.
+## # would raise ValueError if otherwise had free choice
+## self.assertRaises(ValueError, crypt, "hello\x00world", "ab")
+##
+## def test_invalid_salt(self):
+## "test invalid salts"
+## crypt = self.get_crypt()
+##
+## #NOTE: stdlib crypt's behavior is to return "" in this case.
+## # passlib wraps stdlib crypt so it raises ValueError
+## self.assertRaises(ValueError, crypt, "fooey","")
+##
+## #NOTE: stdlib crypt's behavior is rather bizarre in this case
+## # (see wrapper in passlib.unix_crypt).
+## # passlib wraps stdlib crypt so it raises ValueError
+## self.assertRaises(ValueError, crypt, "fooey","f")
+##
+## #FIXME: stdlib crypt does something unpredictable
+## #if passed salt chars outside of H64.CHARS range.
+## #not sure *what* it's algorithm is. should figure that out.
+## # until then, passlib wraps stdlib crypt so this causes ValueError
+## self.assertRaises(ValueError, crypt, "fooey", "a@")
+##
+##if mod.backend != "builtin" and enable_option("fallback-backend"):
+## class BuiltinDesCryptBackendTest(_DesCryptBackendTest):
+## "test builtin des-crypt backend"
+## case_prefix = "builtin des-crypt() backend"
+##
+## def get_crypt(self):
+## return builtin_crypt
+##
+##if enable_option("backends"):
+## #NOTE: this will generally be the stdlib implementation,
+## #which of course is correct, so doing this more to detect deviations in builtin implementation
+## class ActiveDesCryptBackendTest(_DesCryptBackendTest):
+## "test active des-crypt backend"
+## case_prefix = mod.backend + " des-crypt() backend"
+##
+## def get_crypt(self):
+## return mod.crypt
+
+#=========================================================
+#EOF
+#=========================================================
diff --git a/passlib/tests/test_md5_crypt.py b/passlib/tests/test_hash_md5_crypt.py
index 32d969e..ee2edc0 100644
--- a/passlib/tests/test_md5_crypt.py
+++ b/passlib/tests/test_hash_md5_crypt.py
@@ -9,15 +9,16 @@ from logging import getLogger
#site
#pkg
from passlib.tests.handler_utils import _HandlerTestCase
-import passlib.unix.md5_crypt as mod
+import passlib.hash.md5_crypt as mod
+import passlib.hash.apr_md5_crypt as apr
#module
log = getLogger(__name__)
#=========================================================
-#hash alg
+#md5 crypt
#=========================================================
class Md5CryptTest(_HandlerTestCase):
- handler = mod.Md5Crypt
+ handler = mod
known_correct = (
('', '$1$dOHYPKoP$tnxS1T8Q6VVn3kpV8cN6o.'),
@@ -33,6 +34,25 @@ class Md5CryptTest(_HandlerTestCase):
'$1$dOHYPKoP$tnxS1T8Q6VVn3kpV8cN6o!',
)
+#TODO: if os crypt backend selected,
+# should disable temporarily and test builtin backend.
+
+#=========================================================
+#apr md5 crypt
+#=========================================================
+class AprMd5CryptTest(_HandlerTestCase):
+ handler = apr
+
+ #values taken from http://httpd.apache.org/docs/2.2/misc/password_encryptions.html
+ known_correct = (
+ ('myPassword', '$apr1$r31.....$HqJZimcKQFAMYayBlzkrA/'),
+ )
+
+ known_invalid = (
+ #bad char in otherwise correct hash
+ '$apr1$r31.....$HqJZimcKQFAMYayBlzkrA!'
+ )
+
#=========================================================
#EOF
#=========================================================
diff --git a/passlib/tests/test_mysql.py b/passlib/tests/test_hash_mysql.py
index a23a588..d0f97dd 100644
--- a/passlib/tests/test_mysql.py
+++ b/passlib/tests/test_hash_mysql.py
@@ -9,15 +9,16 @@ from logging import getLogger
#site
#pkg
from passlib.tests.handler_utils import _HandlerTestCase
-import passlib.mysql as mod
+import passlib.hash.mysql_323 as mod3
+import passlib.hash.mysql_41 as mod4
#module
log = getLogger(__name__)
#=========================================================
#database hashes
#=========================================================
-class Mysql10CryptTest(_HandlerTestCase):
- handler = mod.Mysql10Crypt
+class Mysql323CryptTest(_HandlerTestCase):
+ handler = mod3
#remove single space from secrets, since mysql-10 DISCARDS WHITESPACE !?!
standard_secrets = [ x for x in _HandlerTestCase.standard_secrets if x != ' ' ]
@@ -37,7 +38,7 @@ class Mysql10CryptTest(_HandlerTestCase):
self.assertEqual(h, h2)
class Mysql41CryptTest(_HandlerTestCase):
- handler = mod.Mysql41Crypt
+ handler = mod4
known_correct = (
('mypass', '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF4'),
)
diff --git a/passlib/tests/test_postgres.py b/passlib/tests/test_hash_postgres.py
index 1a2406c..ebef4e9 100644
--- a/passlib/tests/test_postgres.py
+++ b/passlib/tests/test_hash_postgres.py
@@ -9,7 +9,7 @@ from logging import getLogger
#site
#pkg
from passlib.tests.handler_utils import _HandlerTestCase
-import passlib.postgres as mod
+import passlib.hash.postgres_md5 as mod
#module
log = getLogger(__name__)
@@ -17,7 +17,7 @@ log = getLogger(__name__)
#database hashes
#=========================================================
class PostgresMd5CryptTest(_HandlerTestCase):
- handler = mod.PostgresMd5Crypt
+ handler = mod
known_correct = (
# ((secret,user),hash)
(('mypass', 'postgres'), 'md55fba2ea04fd36069d2574ea71c8efe9d'),
diff --git a/passlib/tests/test_hash_sha_crypt.py b/passlib/tests/test_hash_sha_crypt.py
new file mode 100644
index 0000000..195318e
--- /dev/null
+++ b/passlib/tests/test_hash_sha_crypt.py
@@ -0,0 +1,154 @@
+"""tests for passlib.pwhash -- (c) Assurance Technologies 2003-2009"""
+#=========================================================
+#imports
+#=========================================================
+from __future__ import with_statement
+#core
+import hashlib
+from logging import getLogger
+import re
+try:
+ from warnings import catch_warnings
+except ImportError: #wasn't added until py26
+ catch_warnings = None
+import warnings
+#site
+#pkg
+from passlib.tests.utils import TestCase, enable_option
+from passlib.tests.handler_utils import _HandlerTestCase
+import passlib.hash.sha256_crypt as mod2
+import passlib.hash.sha512_crypt as mod5
+#module
+log = getLogger(__name__)
+
+#=========================================================
+#test sha256-crypt
+#=========================================================
+class Sha256CryptTest(_HandlerTestCase):
+ handler = mod2
+ known_correct = (
+ ('', '$5$rounds=10428$uy/jIAhCetNCTtb0$YWvUOXbkqlqhyoPMpN8BMe.ZGsGx2aBvxTvDFI613c3'),
+ (' ', '$5$rounds=10376$I5lNtXtRmf.OoMd8$Ko3AI1VvTANdyKhBPavaRjJzNpSatKU6QVN9uwS9MH.'),
+ ('test', '$5$rounds=11858$WH1ABM5sKhxbkgCK$aTQsjPkz0rBsH3lQlJxw9HDTDXPKBxC0LlVeV69P.t1'),
+ ('Compl3X AlphaNu3meric', '$5$rounds=10350$o.pwkySLCzwTdmQX$nCMVsnF3TXWcBPOympBUUSQi6LGGloZoOsVJMGJ09UB'),
+ ('4lpHa N|_|M3r1K W/ Cur5Es: #$%(*)(*%#', '$5$rounds=11944$9dhlu07dQMRWvTId$LyUI5VWkGFwASlzntk1RLurxX54LUhgAcJZIt0pYGT7'),
+ (u'with unic\u00D6de', '$5$rounds=1000$IbG0EuGQXw5EkMdP$LQ5AfPf13KufFsKtmazqnzSGZ4pxtUNw3woQ.ELRDF4'),
+ )
+ known_invalid = (
+ #bad char in otherwise correct hash
+ '$5$rounds=10428$uy/:jIAhCetNCTtb0$YWvUOXbkqlqhyoPMpN8BMeZGsGx2aBvxTvDFI613c3'
+ )
+
+if mod2.backend != "builtin" and enable_option("all-backends"):
+
+ #monkeypatch sha256-crypt mod so it uses builtin backend
+
+ class BuiltinSha256CryptTest(Sha256CryptTest):
+ case_prefix = "sha256-crypt (builtin backend)"
+
+ def setUp(self):
+ self.tmp = mod2.crypt
+ mod2.crypt = None
+
+ def cleanUp(self):
+ mod2.crypt = self.tmp
+
+#=========================================================
+#test sha512-crypt
+#=========================================================
+class Sha512CryptTest(_HandlerTestCase):
+ handler = mod5
+ known_correct = (
+ ('', '$6$rounds=11021$KsvQipYPWpr93wWP$v7xjI4X6vyVptJjB1Y02vZC5SaSijBkGmq1uJhPr3cvqvvkd42Xvo48yLVPFt8dvhCsnlUgpX.//Cxn91H4qy1'),
+ (' ', '$6$rounds=11104$ED9SA4qGmd57Fq2m$q/.PqACDM/JpAHKmr86nkPzzuR5.YpYa8ZJJvI8Zd89ZPUYTJExsFEIuTYbM7gAGcQtTkCEhBKmp1S1QZwaXx0'),
+ ('test', '$6$rounds=11531$G/gkPn17kHYo0gTF$Kq.uZBHlSBXyzsOJXtxJruOOH4yc0Is13uY7yK0PvAvXxbvc1w8DO1RzREMhKsc82K/Jh8OquV8FZUlreYPJk1'),
+ ('Compl3X AlphaNu3meric', '$6$rounds=10787$wakX8nGKEzgJ4Scy$X78uqaX1wYXcSCtS4BVYw2trWkvpa8p7lkAtS9O/6045fK4UB2/Jia0Uy/KzCpODlfVxVNZzCCoV9s2hoLfDs/'),
+ ('4lpHa N|_|M3r1K W/ Cur5Es: #$%(*)(*%#', '$6$rounds=11065$5KXQoE1bztkY5IZr$Jf6krQSUKKOlKca4hSW07MSerFFzVIZt/N3rOTsUgKqp7cUdHrwV8MoIVNCk9q9WL3ZRMsdbwNXpVk0gVxKtz1'),
+ )
+ known_invalid = (
+ #bad char in otherwise correct hash
+ '$6$rounds=11021$KsvQipYPWpr9:wWP$v7xjI4X6vyVptJjB1Y02vZC5SaSijBkGmq1uJhPr3cvqvvkd42Xvo48yLVPFt8dvhCsnlUgpX.//Cxn91H4qy1',
+ )
+
+ #NOTE: these test cases taken from spec definition at http://www.akkadia.org/drepper/SHA-crypt.txt
+ cases512 = [
+ #salt-hash, secret, result
+ ("$6$saltstring", "Hello world!",
+ "$6$saltstring$svn8UoSVapNtMuq1ukKS4tPQd8iKwSMHWjl/O817G3uBnIFNjnQJu"
+ "esI68u4OTLiBFdcbYEdFCoEOfaS35inz1" ),
+
+ ( "$6$rounds=10000$saltstringsaltstring", "Hello world!",
+ "$6$rounds=10000$saltstringsaltst$OW1/O6BYHV6BcXZu8QVeXbDWra3Oeqh0sb"
+ "HbbMCVNSnCM/UrjmM0Dp8vOuZeHBy/YTBmSK6H9qs/y3RnOaw5v." ),
+
+ ( "$6$rounds=5000$toolongsaltstring", "This is just a test",
+ "$6$rounds=5000$toolongsaltstrin$lQ8jolhgVRVhY4b5pZKaysCLi0QBxGoNeKQ"
+ "zQ3glMhwllF7oGDZxUhx1yxdYcz/e1JSbq3y6JMxxl8audkUEm0" ),
+
+ ( "$6$rounds=1400$anotherlongsaltstring",
+ "a very much longer text to encrypt. This one even stretches over more"
+ "than one line.",
+ "$6$rounds=1400$anotherlongsalts$POfYwTEok97VWcjxIiSOjiykti.o/pQs.wP"
+ "vMxQ6Fm7I6IoYN3CmLs66x9t0oSwbtEW7o7UmJEiDwGqd8p4ur1" ),
+
+ ( "$6$rounds=77777$short",
+ "we have a short salt string but not a short password",
+ "$6$rounds=77777$short$WuQyW2YR.hBNpjjRhpYD/ifIw05xdfeEyQoMxIXbkvr0g"
+ "ge1a1x3yRULJ5CCaUeOxFmtlcGZelFl5CxtgfiAc0" ),
+
+ ( "$6$rounds=123456$asaltof16chars..", "a short string",
+ "$6$rounds=123456$asaltof16chars..$BtCwjqMJGx5hrJhZywWvt0RLE8uZ4oPwc"
+ "elCjmw2kSYu.Ec6ycULevoBK25fs2xXgMNrCzIMVcgEJAstJeonj1" ),
+
+ ( "$6$rounds=10$roundstoolow", "the minimum number is still observed",
+ "$6$rounds=1000$roundstoolow$kUMsbe306n21p9R.FRkW3IGn.S9NPN0x50YhH1x"
+ "hLsPuWGsUSklZt58jaTfF4ZEQpyUNGc0dqbpBYYBaHHrsX." ),
+ ]
+
+ def test_spec_vectors(self):
+ "verify sha512-crypt passes specification test vectors"
+ handler = mod5
+
+ #NOTE: the 'roundstoolow' vector is known to raise a warning, which we silence here
+ if catch_warnings:
+ ctx = catch_warnings()
+ ctx.__enter__()
+ warnings.filterwarnings("ignore", "sha512-crypt algorithm does not allow less than 1000 rounds: 10")
+
+ for config, secret, hash in self.cases512:
+
+ result = handler.genhash(secret, config)
+
+ #parse config
+ settings = handler.parse(config)
+
+ #make sure we got expected result back
+ self.assertEqual(result, hash, "hash=%r secret=%r:" % (hash, secret))
+
+ #parse result and check that salt was truncated to max 16 chars
+ info = handler.parse(result)
+ if len(settings['salt']) > 16:
+ #spec sez we can truncate salt
+ self.assertEqual(info['salt'], settings['salt'][:16], "hash=%r secret=%r:" % (hash, secret))
+ else:
+ self.assertEqual(info['salt'], settings['salt'], "hash=%r secret=%r:" % (hash, secret))
+
+ if catch_warnings:
+ ctx.__exit__(None,None,None)
+
+if mod5.backend != "builtin" and enable_option("all-backends"):
+
+ #monkeypatch sha512-crypt mod so it uses builtin backend
+
+ class BuiltinSha512CryptTest(Sha512CryptTest):
+ case_prefix = "sha512-crypt (builtin backend)"
+
+ def setUp(self):
+ self.tmp = mod5.crypt
+ mod5.crypt = None
+
+ def cleanUp(self):
+ mod5.crypt = self.tmp
+#=========================================================
+#EOF
+#=========================================================
diff --git a/passlib/tests/test_sun_md5_crypt.py b/passlib/tests/test_hash_sun_md5_crypt.py
index c17480d..2cc6f70 100644
--- a/passlib/tests/test_sun_md5_crypt.py
+++ b/passlib/tests/test_hash_sun_md5_crypt.py
@@ -9,7 +9,7 @@ from logging import getLogger
#site
#pkg
from passlib.tests.handler_utils import _HandlerTestCase
-import passlib.unix.sun_md5_crypt as mod
+import passlib.hash.sun_md5_crypt as mod
#module
log = getLogger(__name__)
@@ -17,7 +17,7 @@ log = getLogger(__name__)
#hash alg
#=========================================================
class SunMd5CryptTest(_HandlerTestCase):
- handler = mod.SunMd5Crypt
+ handler = mod
known_correct = [
("passwd", "$md5$RPgLF6IJ$WTvAlUJ7MqH5xak2FMEwS/"),
diff --git a/passlib/tests/test_sha_crypt.py b/passlib/tests/test_sha_crypt.py
deleted file mode 100644
index 84d136c..0000000
--- a/passlib/tests/test_sha_crypt.py
+++ /dev/null
@@ -1,123 +0,0 @@
-"""tests for passlib.pwhash -- (c) Assurance Technologies 2003-2009"""
-#=========================================================
-#imports
-#=========================================================
-from __future__ import with_statement
-#core
-import hashlib
-from logging import getLogger
-import re
-#site
-#pkg
-from passlib.tests.utils import TestCase, enable_test
-from passlib.tests.handler_utils import _HandlerTestCase
-import passlib.unix.sha_crypt as mod
-#module
-log = getLogger(__name__)
-
-#=========================================================
-#test raw sha-crypt implementation
-#=========================================================
-if enable_test("backends"):
-
- class Sha512BackendTest(TestCase):
- "test sha512-crypt backend against specification unittest"
- case_prefix = "sha-crypt backend"
-
- #NOTE: these test cases taken from spec definition at http://www.akkadia.org/drepper/SHA-crypt.txt
- cases512 = [
- #salt-hash, secret, result
- ("$6$saltstring", "Hello world!",
- "$6$saltstring$svn8UoSVapNtMuq1ukKS4tPQd8iKwSMHWjl/O817G3uBnIFNjnQJu"
- "esI68u4OTLiBFdcbYEdFCoEOfaS35inz1" ),
-
- ( "$6$rounds=10000$saltstringsaltstring", "Hello world!",
- "$6$rounds=10000$saltstringsaltst$OW1/O6BYHV6BcXZu8QVeXbDWra3Oeqh0sb"
- "HbbMCVNSnCM/UrjmM0Dp8vOuZeHBy/YTBmSK6H9qs/y3RnOaw5v." ),
-
- ( "$6$rounds=5000$toolongsaltstring", "This is just a test",
- "$6$rounds=5000$toolongsaltstrin$lQ8jolhgVRVhY4b5pZKaysCLi0QBxGoNeKQ"
- "zQ3glMhwllF7oGDZxUhx1yxdYcz/e1JSbq3y6JMxxl8audkUEm0" ),
-
- ( "$6$rounds=1400$anotherlongsaltstring",
- "a very much longer text to encrypt. This one even stretches over more"
- "than one line.",
- "$6$rounds=1400$anotherlongsalts$POfYwTEok97VWcjxIiSOjiykti.o/pQs.wP"
- "vMxQ6Fm7I6IoYN3CmLs66x9t0oSwbtEW7o7UmJEiDwGqd8p4ur1" ),
-
- ( "$6$rounds=77777$short",
- "we have a short salt string but not a short password",
- "$6$rounds=77777$short$WuQyW2YR.hBNpjjRhpYD/ifIw05xdfeEyQoMxIXbkvr0g"
- "ge1a1x3yRULJ5CCaUeOxFmtlcGZelFl5CxtgfiAc0" ),
-
- ( "$6$rounds=123456$asaltof16chars..", "a short string",
- "$6$rounds=123456$asaltof16chars..$BtCwjqMJGx5hrJhZywWvt0RLE8uZ4oPwc"
- "elCjmw2kSYu.Ec6ycULevoBK25fs2xXgMNrCzIMVcgEJAstJeonj1" ),
-
- ( "$6$rounds=10$roundstoolow", "the minimum number is still observed",
- "$6$rounds=1000$roundstoolow$kUMsbe306n21p9R.FRkW3IGn.S9NPN0x50YhH1x"
- "hLsPuWGsUSklZt58jaTfF4ZEQpyUNGc0dqbpBYYBaHHrsX." ),
- ]
- def test512(self):
- "verify sha512 passes specification test vectors"
- handler = mod.Sha512Crypt
- _raw_encrypt = mod.raw_sha512_crypt
-
- def raw_encrypt(secret, salt, rounds, implicit_rounds=False, ident=None):
- checksum, salt, rounds = _raw_encrypt(secret, salt, rounds)
- return handler.render(rounds, salt, checksum, implicit_rounds)
-
- for hash, secret, result in self.cases512:
-
- #parse salt
- rec = handler.parse_config(hash)
-
- #encrypt secret, preserving rounds & salt
- out = raw_encrypt(secret, **rec)
-
- #make sure we got expected result back
- self.assertEqual(out, result, "hash=%r secret=%r:" % (hash, secret))
-
- #parse result and check that salt was truncated to max 16 chars
- rec2 = handler.parse(out)
- if len(rec['salt']) > 16:
- #spec sez we can truncate salt
- self.assertEqual(rec2['salt'], rec['salt'][:16], "hash=%r secret=%r:" % (hash, secret))
- else:
- self.assertEqual(rec2['salt'], rec['salt'], "hash=%r secret=%r:" % (hash, secret))
-
-#=========================================================
-#test frontend classes
-#=========================================================
-class Sha256CryptTest(_HandlerTestCase):
- handler = mod.Sha256Crypt
- known_correct = (
- ('', '$5$rounds=10428$uy/jIAhCetNCTtb0$YWvUOXbkqlqhyoPMpN8BMe.ZGsGx2aBvxTvDFI613c3'),
- (' ', '$5$rounds=10376$I5lNtXtRmf.OoMd8$Ko3AI1VvTANdyKhBPavaRjJzNpSatKU6QVN9uwS9MH.'),
- ('test', '$5$rounds=11858$WH1ABM5sKhxbkgCK$aTQsjPkz0rBsH3lQlJxw9HDTDXPKBxC0LlVeV69P.t1'),
- ('Compl3X AlphaNu3meric', '$5$rounds=10350$o.pwkySLCzwTdmQX$nCMVsnF3TXWcBPOympBUUSQi6LGGloZoOsVJMGJ09UB'),
- ('4lpHa N|_|M3r1K W/ Cur5Es: #$%(*)(*%#', '$5$rounds=11944$9dhlu07dQMRWvTId$LyUI5VWkGFwASlzntk1RLurxX54LUhgAcJZIt0pYGT7'),
- (u'with unic\u00D6de', '$5$rounds=1000$IbG0EuGQXw5EkMdP$LQ5AfPf13KufFsKtmazqnzSGZ4pxtUNw3woQ.ELRDF4'),
- )
- known_invalid = (
- #bad char in otherwise correct hash
- '$5$rounds=10428$uy/:jIAhCetNCTtb0$YWvUOXbkqlqhyoPMpN8BMeZGsGx2aBvxTvDFI613c3'
- )
-
-class Sha512CryptTest(_HandlerTestCase):
- handler = mod.Sha512Crypt
- known_correct = (
- ('', '$6$rounds=11021$KsvQipYPWpr93wWP$v7xjI4X6vyVptJjB1Y02vZC5SaSijBkGmq1uJhPr3cvqvvkd42Xvo48yLVPFt8dvhCsnlUgpX.//Cxn91H4qy1'),
- (' ', '$6$rounds=11104$ED9SA4qGmd57Fq2m$q/.PqACDM/JpAHKmr86nkPzzuR5.YpYa8ZJJvI8Zd89ZPUYTJExsFEIuTYbM7gAGcQtTkCEhBKmp1S1QZwaXx0'),
- ('test', '$6$rounds=11531$G/gkPn17kHYo0gTF$Kq.uZBHlSBXyzsOJXtxJruOOH4yc0Is13uY7yK0PvAvXxbvc1w8DO1RzREMhKsc82K/Jh8OquV8FZUlreYPJk1'),
- ('Compl3X AlphaNu3meric', '$6$rounds=10787$wakX8nGKEzgJ4Scy$X78uqaX1wYXcSCtS4BVYw2trWkvpa8p7lkAtS9O/6045fK4UB2/Jia0Uy/KzCpODlfVxVNZzCCoV9s2hoLfDs/'),
- ('4lpHa N|_|M3r1K W/ Cur5Es: #$%(*)(*%#', '$6$rounds=11065$5KXQoE1bztkY5IZr$Jf6krQSUKKOlKca4hSW07MSerFFzVIZt/N3rOTsUgKqp7cUdHrwV8MoIVNCk9q9WL3ZRMsdbwNXpVk0gVxKtz1'),
- )
- known_invalid = (
- #bad char in otherwise correct hash
- '$6$rounds=11021$KsvQipYPWpr9:wWP$v7xjI4X6vyVptJjB1Y02vZC5SaSijBkGmq1uJhPr3cvqvvkd42Xvo48yLVPFt8dvhCsnlUgpX.//Cxn91H4qy1',
- )
-
-#=========================================================
-#EOF
-#=========================================================
diff --git a/passlib/tests/test_utils.py b/passlib/tests/test_utils.py
index 83bc730..26ceb02 100644
--- a/passlib/tests/test_utils.py
+++ b/passlib/tests/test_utils.py
@@ -3,12 +3,15 @@
#imports
#=========================================================
#core
+from binascii import hexlify
import sys
import random
#site
#pkg
#module
from passlib import utils
+from passlib.utils import h64, des
+from passlib.utils.md4 import md4
from passlib.tests.utils import TestCase, Params as ak
#=========================================================
#byte funcs
@@ -88,27 +91,82 @@ class BytesTest(TestCase):
self.assertEqual(utils.bytes_to_list('\x00\x00\x01', order="native"), [0, 0, 1])
#=========================================================
+#test des library
+#=========================================================
+class DesTest(TestCase):
+
+ #test vectors taken from http://www.skepticfiles.org/faq/testdes.htm
+
+ #(key, plaintext, ciphertext) all as 64 bit
+ test_des_vectors = [
+ (int(line[4:21],16), int(line[21:38],16), int(line[38:],16))
+ for line in
+ """ 0000000000000000 0000000000000000 8CA64DE9C1B123A7
+ FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF 7359B2163E4EDC58
+ 3000000000000000 1000000000000001 958E6E627A05557B
+ 1111111111111111 1111111111111111 F40379AB9E0EC533
+ 0123456789ABCDEF 1111111111111111 17668DFC7292532D
+ 1111111111111111 0123456789ABCDEF 8A5AE1F81AB8F2DD
+ 0000000000000000 0000000000000000 8CA64DE9C1B123A7
+ FEDCBA9876543210 0123456789ABCDEF ED39D950FA74BCC4
+ 7CA110454A1A6E57 01A1D6D039776742 690F5B0D9A26939B
+ 0131D9619DC1376E 5CD54CA83DEF57DA 7A389D10354BD271
+ 07A1133E4A0B2686 0248D43806F67172 868EBB51CAB4599A
+ 3849674C2602319E 51454B582DDF440A 7178876E01F19B2A
+ 04B915BA43FEB5B6 42FD443059577FA2 AF37FB421F8C4095
+ 0113B970FD34F2CE 059B5E0851CF143A 86A560F10EC6D85B
+ 0170F175468FB5E6 0756D8E0774761D2 0CD3DA020021DC09
+ 43297FAD38E373FE 762514B829BF486A EA676B2CB7DB2B7A
+ 07A7137045DA2A16 3BDD119049372802 DFD64A815CAF1A0F
+ 04689104C2FD3B2F 26955F6835AF609A 5C513C9C4886C088
+ 37D06BB516CB7546 164D5E404F275232 0A2AEEAE3FF4AB77
+ 1F08260D1AC2465E 6B056E18759F5CCA EF1BF03E5DFA575A
+ 584023641ABA6176 004BD6EF09176062 88BF0DB6D70DEE56
+ 025816164629B007 480D39006EE762F2 A1F9915541020B56
+ 49793EBC79B3258F 437540C8698F3CFA 6FBF1CAFCFFD0556
+ 4FB05E1515AB73A7 072D43A077075292 2F22E49BAB7CA1AC
+ 49E95D6D4CA229BF 02FE55778117F12A 5A6B612CC26CCE4A
+ 018310DC409B26D6 1D9D5C5018F728C2 5F4C038ED12B2E41
+ 1C587F1C13924FEF 305532286D6F295A 63FAC0D034D9F793
+ 0101010101010101 0123456789ABCDEF 617B3A0CE8F07100
+ 1F1F1F1F0E0E0E0E 0123456789ABCDEF DB958605F8C8C606
+ E0FEE0FEF1FEF1FE 0123456789ABCDEF EDBFD1C66C29CCC7
+ 0000000000000000 FFFFFFFFFFFFFFFF 355550B2150E2451
+ FFFFFFFFFFFFFFFF 0000000000000000 CAAAAF4DEAF1DBAE
+ 0123456789ABCDEF 0000000000000000 D5D44FF720683D0D
+ FEDCBA9876543210 FFFFFFFFFFFFFFFF 2A2BB008DF97C2F2
+ """.split("\n") if line.strip()
+ ]
+
+ def test_des_encrypt_int_block(self):
+ for k,p,c in self.test_des_vectors:
+ result = des.des_encrypt_int_block(k,p)
+ self.assertEqual(result, c, "key=%r p=%r:" % (k,p))
+
+ #TODO: test other des methods (eg: mdes_encrypt_int_block)
+
+#=========================================================
#hash64
#=========================================================
-class Test_H64(TestCase):
+class H64_Test(TestCase):
"test H64 codec functions"
case_prefix = "H64 codec"
def test_encode_1_offset(self):
- self.assertFunctionResults(utils.h64_encode_1_offset,[
+ self.assertFunctionResults(h64.encode_1_offset,[
("z1", "\xff", 0),
("..", "\x00", 0),
])
def test_encode_2_offsets(self):
- self.assertFunctionResults(utils.h64_encode_2_offsets,[
+ self.assertFunctionResults(h64.encode_2_offsets,[
(".wD", "\x00\xff", 0, 1),
("z1.", "\xff\x00", 0, 1),
("z1.", "\x00\xff", 1, 0),
])
def test_encode_3_offsets(self):
- self.assertFunctionResults(utils.h64_encode_3_offsets,[
+ self.assertFunctionResults(h64.encode_3_offsets,[
#move through each byte, keep offsets
("..kz", "\x00\x00\xff", 0, 1, 2),
(".wD.", "\x00\xff\x00", 0, 1, 2),
@@ -119,21 +177,47 @@ class Test_H64(TestCase):
("z1..", "\x00\x00\xff", 2, 0, 1),
])
- ##def test_randstr(self):
- ## #override default rng so we can get predictable values
- ## rng = random.Random()
- ## def wrapper(*a, **k):
- ## rng.seed(1234)
- ## k['rng'] = rng
- ## return util.h64_gensalt(*a, **k)
- ## self.assertFunctionResults(wrapper,[
- ## ("", 0),
- ## ("x", 1),
- ## ("xQ", 2),
- ## ("xQ.uwZe3lD/mKbb7", 16),
- ## ("xQ.uwZe3lD/mKbb795.Tx2WRa3ZFXdSK", 32),
- ## ])
+ #TODO: test other h64 methods
+#=========================================================
+#test md4
+#=========================================================
+class MD4_Test(TestCase):
+ #test vectors from http://www.faqs.org/rfcs/rfc1320.html - A.5
+
+ vectors = [
+ # input -> hex digest
+ ("", "31d6cfe0d16ae931b73c59d7e0c089c0"),
+ ("a", "bde52cb31de33e46245e05fbdbd6fb24"),
+ ("abc", "a448017aaf21d8525fc10ae87aa6729d"),
+ ("message digest", "d9130a8164549fe818874806e1c7014b"),
+ ("abcdefghijklmnopqrstuvwxyz", "d79e1c308aa5bbcdeea8ed63df412da9"),
+ ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", "043f8582f241db351ce627e153e7f0e4"),
+ ("12345678901234567890123456789012345678901234567890123456789012345678901234567890", "e33b4ddc9c38f2199c3e7b164fcc0536"),
+ ]
+
+ def test_md4_update(self):
+ "test md4 update"
+ h = md4('')
+ self.assertEqual(h.hexdigest(), "31d6cfe0d16ae931b73c59d7e0c089c0")
+
+ h.update('a')
+ self.assertEqual(h.hexdigest(), "bde52cb31de33e46245e05fbdbd6fb24")
+
+ h.update('bcdefghijklmnopqrstuvwxyz')
+ self.assertEqual(h.hexdigest(), "d79e1c308aa5bbcdeea8ed63df412da9")
+
+ def test_md4_hexdigest(self):
+ "test md4 hexdigest()"
+ for input, hex in self.vectors:
+ out = md4(input).hexdigest()
+ self.assertEqual(out, hex)
+
+ def test_md4_digest(self):
+ "test md4 digest()"
+ for input, hex in self.vectors:
+ out = md4(input).digest()
+ self.assertEqual(hexlify(out), hex)
#=========================================================
#EOF
diff --git a/passlib/tests/test_utils_pbkdf2.py b/passlib/tests/test_utils_pbkdf2.py
index ee7b61d..ba05a88 100644
--- a/passlib/tests/test_utils_pbkdf2.py
+++ b/passlib/tests/test_utils_pbkdf2.py
@@ -10,7 +10,7 @@ import hmac
from logging import getLogger
#site
#pkg
-from passlib.tests.utils import TestCase, enable_test
+from passlib.tests.utils import TestCase, enable_option
import passlib.utils.pbkdf2 as mod
#module
log = getLogger(__name__)
@@ -118,12 +118,12 @@ class _Pbkdf2BackendTest(TestCase):
),
])
-if enable_test("fallback-backends" if mod._EVP else "backends"):
+if enable_option("all-backends" if mod._EVP else "backends"):
class Builtin_Pbkdf2BackendTest(_Pbkdf2BackendTest):
case_prefix = "builtin pbkdf2() backend"
disable_m2crypto = True
-if mod._EVP and enable_test("backends"):
+if mod._EVP and enable_option("backends"):
class M2Crypto_Pbkdf2BackendTest(_Pbkdf2BackendTest):
case_prefix = "m2crypto pbkdf2() backend"
diff --git a/passlib/tests/test_win32.py b/passlib/tests/test_win32.py
new file mode 100644
index 0000000..57dc882
--- /dev/null
+++ b/passlib/tests/test_win32.py
@@ -0,0 +1,41 @@
+"""tests for passlib.win32 -- (c) Assurance Technologies 2003-2009"""
+#=========================================================
+#imports
+#=========================================================
+#core
+from binascii import hexlify
+#site
+#pkg
+from passlib.tests.utils import TestCase
+#module
+import passlib.win32 as mod
+
+#=========================================================
+#
+#=========================================================
+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 test_lmhash(self):
+ for secret, hash in [
+ ("OLDPASSWORD", "c9b81d939d6fd80cd408e6b105741864"),
+ ("NEWPASSWORD", '09eeab5aa415d6e4d408e6b105741864'),
+ ("welcome", "c23413a8a1e7665faad3b435b51404ee"),
+ ]:
+ result = mod.raw_lmhash(secret, hex=True)
+ self.assertEquals(result, hash)
+
+ def test_nthash(self):
+ for secret, hash in [
+ ("OLDPASSWORD", "6677b2c394311355b54f25eec5bfacf5"),
+ ("NEWPASSWORD", "256781a62031289d3c2c98c14f1efc8c"),
+ ]:
+ result = mod.raw_nthash(secret, hex=True)
+ self.assertEquals(result, hash)
+
+#=========================================================
+#EOF
+#=========================================================
diff --git a/passlib/tests/utils.py b/passlib/tests/utils.py
index e58d0b0..d2d1447 100644
--- a/passlib/tests/utils.py
+++ b/passlib/tests/utils.py
@@ -11,7 +11,7 @@ import logging; log = logging.getLogger(__name__)
__all__ = [
'TestCase',
'Param',
- 'enable_test',
+ 'enable_option',
]
#=========================================================
@@ -154,7 +154,7 @@ class TestCase(unittest.TestCase):
#helper funcs
#=========================================================
-DEFAULT_TESTS = "backends"
+DEFAULT_TESTS = "active-backends"
tests = [
v.strip()
@@ -162,13 +162,13 @@ tests = [
in os.environ.get("PASSLIB_TESTS", DEFAULT_TESTS).lower().split(",")
]
-def enable_test(*names):
+def enable_option(*names):
"""check if a given test should be included based on the env var.
test flags:
all run ALL tests
- backends test active backends
- fallback-backends test inactive backends
+ active-backends test active backends
+ all-backends test ALL backends, even the inactive ones
slow required to enable really slow tests (eg builtin bcrypt backend)
"""
diff --git a/passlib/unix/__init__.py b/passlib/unix.py
index 3035608..91a202b 100644
--- a/passlib/unix/__init__.py
+++ b/passlib/unix.py
@@ -3,17 +3,7 @@
#=========================================================
#import standard hash algorithms
#=========================================================
-import passlib.unix.des_crypt #registers "des-crypt", "ext-des-crypt" handlers
-
-#XXX: passlib.unix.sun_md5 is working, but hasn't been tested much,
-# so isn't imported by default.
-
-#TODO: Mac OSX salted sha1 hashes - need reference
-#http://www.dribin.org/dave/blog/archives/2006/04/28/os_x_passwords_2/
-
-import passlib.unix.md5_crypt #registers "md5-crypt" handler
-import passlib.unix.bcrypt #registers "bcrypt" handler
-import passlib.unix.sha_crypt #registers "sha256-crypt" and "sha512-crypt" handlers
+from passlib.hash import des_crypt, md5_crypt, bcrypt, sha256_crypt, sha512_crypt
#other recognizers for shadow - NullHandler for empty string (always verify) and "*" (never verify)
#also, a UnknownCryptHandler - given hash, detect if system crypt recognizes it,
@@ -25,13 +15,11 @@ import passlib.unix.sha_crypt #registers "sha256-crypt" and "sha512-crypt" handl
from passlib.context import CryptContext
#default context for quick use.. recognizes common algorithms, uses SHA-512 as default
-default_context = CryptContext(["des-crypt", "md5-crypt", "bcrypt", "sha256-crypt", "sha512-crypt"])
+default_context = CryptContext([des_crypt, md5_crypt, bcrypt, sha256_crypt, sha512_crypt])
#some general os-context helpers (these may not match your os policy exactly, but are generally useful)
-linux_context = CryptContext([ "des-crypt", "md5-crypt", "sha256-crypt", "sha512-crypt" ])
-bsd_context = CryptContext([ "des-crypt", "md5-crypt", "bcrypt" ])
-
-
+linux_context = CryptContext([ des_crypt, md5_crypt, sha256_crypt, sha512_crypt ])
+bsd_context = CryptContext([ des_crypt, md5_crypt, bcrypt ])
#=========================================================
#eof
diff --git a/passlib/unix/bcrypt.py b/passlib/unix/bcrypt.py
deleted file mode 100644
index cf1a5af..0000000
--- a/passlib/unix/bcrypt.py
+++ /dev/null
@@ -1,148 +0,0 @@
-"""passlib.bcrypt"""
-#=========================================================
-#imports
-#=========================================================
-from __future__ import with_statement, absolute_import
-#core
-import re
-import logging; log = logging.getLogger(__name__)
-from warnings import warn
-#site
-#libs
-from passlib.handler import ExtCryptHandler, register_crypt_handler
-#pkg
-#local
-__all__ = [
- "BCrypt",
-## "bcrypt", "backend",
-]
-
-#=========================================================
-#load bcrypt backend
-#=========================================================
-#fall back to our much slower pure-python implementation
-from passlib.utils._slow_bcrypt import hashpw as bcrypt
-backend = "builtin"
-
-try:
- #try importing py-bcrypt, it's much faster
- from bcrypt import hashpw as bcrypt
- backend = "pybcrypt"
-except ImportError:
- #check for OS crypt support before falling back to pure python version
- try:
- from crypt import crypt
- except ImportError:
- pass
- else:
- if (
- crypt("test", "$2a$04$......................") == '$2a$04$......................qiOQjkB8hxU8OzRhS.GhRMa4VUnkPty'
- and
- crypt("test", "$2$04$......................") == '$2$04$......................1O4gOrCYaqBG3o/4LnT2ykQUt1wbyju'
- ):
- def bcrypt(secret, config):
- if isinstance(secret, unicode):
- secret = secret.encode("utf-8")
- hash = crypt(secret, config)
- if not hash.startswith("$2a$") and not hash.startswith("$2$"):
- #means config was wrong
- raise ValueError, "not a bcrypt hash"
- return hash
- backend = "stdlib"
-
-#XXX: should issue warning when _slow_bcrypt is first used.
-
-#=========================================================
-#OpenBSD's BCrypt
-#=========================================================
-class BCrypt(ExtCryptHandler):
- """Implementation of OpenBSD's BCrypt algorithm.
-
- Passlib will use the py-bcrypt package if it is available,
- otherwise it will fall back to a slower builtin pure-python implementation.
-
- Note that rounds must be >= 10 or an error will be returned.
-
- .. automethod:: encrypt
- """
- #=========================================================
- #algorithm info
- #=========================================================
- name = "bcrypt"
- #stats: 192 bit checksum, 128 bit salt, 2**(4..31) rounds, max 72 chars of secret
-
- setting_kwds = ("salt", "rounds")
-
- salt_chars = 22
-
- default_rounds = 12
- min_rounds = 4 # pybcrypt won't take less than this
- max_rounds = 31 # 32-bit limitation on 1<<rounds
-
- #=========================================================
- #helpers
- #=========================================================
- _pat = re.compile(r"""
- ^
- \$(?P<ident>2a?)
- \$(?P<rounds>\d+)
- \$(?P<salt>[A-Za-z0-9./]{22})
- (?P<chk>[A-Za-z0-9./]{31})?
- $
- """, re.X)
-
- @classmethod
- def parse(cls, hash):
- if not hash:
- raise ValueError, "no hash specified"
- m = cls._pat.match(hash)
- if not m:
- raise ValueError, "invalid bcrypt hash"
- ident, rounds, salt, chk = m.group("ident", "rounds", "salt", "chk")
- out = dict(
- rounds=int(rounds),
- salt=salt,
- checksum=chk,
- )
- if ident == '2':
- out['omit_null_suffix'] = True
- return out
-
- @classmethod
- def render(cls, rounds, salt, checksum=None, omit_null_suffix=False):
- if omit_null_suffix:
- out = "$2$%d$%s" % (rounds, salt)
- else:
- out = "$2a$%d$%s" % (rounds, salt)
- if checksum is not None:
- out += "$" + checksum
- return out
-
- #=========================================================
- #frontend
- #=========================================================
- @classmethod
- def identify(cls, hash):
- "identify bcrypt hash"
- return bool(hash and cls._pat.match(hash))
-
- @classmethod
- def genconfig(cls, salt=None, rounds=None, omit_null_suffix=False):
- salt = cls._norm_salt(salt)
- rounds = cls._norm_rounds(rounds)
- return cls.render(rounds, salt, None, omit_null_suffix)
-
- @classmethod
- def genhash(cls, secret, config):
- config = cls._norm_config(config)
- return bcrypt(secret, config)
-
- #=========================================================
- #eoc
- #=========================================================
-
-register_crypt_handler(BCrypt)
-
-#=========================================================
-# eof
-#=========================================================
diff --git a/passlib/unix/des_crypt.py b/passlib/unix/des_crypt.py
deleted file mode 100644
index 8fa1601..0000000
--- a/passlib/unix/des_crypt.py
+++ /dev/null
@@ -1,247 +0,0 @@
-"""passlib - implementation of various password hashing functions
-
-http://www.phpbuilder.com/manual/function.crypt.php
-
-http://dropsafe.crypticide.com/article/1389
-"""
-#=========================================================
-#imports
-#=========================================================
-from __future__ import with_statement
-#core
-import inspect
-import re
-import hashlib
-import logging; log = logging.getLogger(__name__)
-import time
-import os
-#site
-#pkg
-from passlib.utils import H64_CHARS
-from passlib.handler import ExtCryptHandler, register_crypt_handler
-#local
-__all__ = [
- 'DesCrypt',
- 'ExtDesCrypt',
-]
-
-#=========================================================
-#load unix crypt backend
-#=========================================================
-try:
- #try stdlib module, which is only present under posix
- from crypt import crypt as _crypt
- if not _crypt("test", "ab") == 'abgOeLfPimXQo':
- #shouldn't be any unix os which has crypt but doesn't support this format.
- raise EnvironmentError, "crypt() failed runtime test for DES-CRYPT support"
-
- #NOTE: we're wrapping the builtin crypt with some checks due to deficiencies in it's base implementation.
- # 1. given an empty salt, it returns '' instead of raising an error. the wrapper raises an error.
- # 2. given a single letter salt, it returns a hash with the original salt doubled,
- # but appears to calculate the hash based on the letter + "G" as the second byte.
- # this results in a hash that won't validate, which is DEFINITELY wrong.
- # the wrapper raises an error.
- # 3. given salt chars outside of H64_CHARS range, it does something unknown internally,
- # but reports the hashes correctly. until this alg gets fixed in builtin crypt or stdlib crypt,
- # wrapper raises an error for bad salts.
- # 4. it tries to encode unicode -> ascii, unlike most hashes. the wrapper encodes to utf-8.
- def crypt(key, salt):
- "wrapper around stdlib's crypt"
- if '\x00' in key:
- raise ValueError, "null char in key"
- if isinstance(key, unicode):
- key = key.encode("utf-8")
- if not salt:
- raise ValueError, "no salt specified"
- if len(salt) < 2:
- raise ValueError, "salt must have 2 chars"
- elif len(salt) > 2:
- salt = salt[:2]
- for c in salt:
- if c not in H64_CHARS:
- raise ValueError, "invalid char in salt"
- return _crypt(key, salt)
-
- backend = "stdlib"
-except ImportError:
- #XXX: could check for openssl passwd -des support in libssl
-
-
- #TODO: need to reconcile our implementation's behavior
- # with the stdlib's behavior so error types, messages, and limitations
- # are the same. (eg: handling of None and unicode chars)
- from passlib.utils._slow_des_crypt import crypt
- backend = "builtin"
-
-from passlib.utils._slow_des_crypt import raw_ext_crypt, b64_decode_int24, b64_encode_int24
-
-#=========================================================
-#old unix crypt
-#=========================================================
-class DesCrypt(ExtCryptHandler):
- """Old Unix-Crypt Algorithm, as originally used on unix before md5-crypt arrived.
- This implementation uses the builtin ``crypt`` module when available,
- but contains a pure-python fallback so that this algorithm can always be used.
- """
- #=========================================================
- #crypt information
- #=========================================================
- name = "des-crypt"
- aliases = ("unix-crypt",)
-
- #stats: 66 bit checksum, 12 bit salt, max 8 chars of secret
-
- setting_kwds = ("salt")
-
- salt_chars = 2
-
- #=========================================================
- #helpers
- #=========================================================
-
- #FORMAT: 2 chars of H64-encoded salt + 11 chars of H64-encoded checksum
- _pat = re.compile(r"""
- ^
- (?P<salt>[./a-z0-9]{2})
- (?P<chk>[./a-z0-9]{11})?
- $""", re.X|re.I)
-
- @classmethod
- def parse(cls, hash):
- if not hash:
- raise ValueError, "no des-crypt hash specified"
- m = cls._pat.match(hash)
- if not m:
- raise ValueError, "not a des-crypt hash"
- return dict(
- salt=m.group("salt"),
- checksum=m.group("chk")
- )
-
- @classmethod
- def render(cls, salt, checksum=None):
- if len(salt) < 2:
- raise ValueError, "invalid salt"
- return "%s%s" % (salt[:2], checksum or '')
-
- #=========================================================
- #frontend
- #=========================================================
- @classmethod
- def identify(cls, hash):
- return bool(hash and cls._pat.match(hash))
-
- @classmethod
- def genconfig(cls, salt=None):
- return cls._norm_salt(salt)
-
- @classmethod
- def genhash(cls, secret, config):
- config = cls._norm_config(config)
- return crypt(secret, config)
-
- #=========================================================
- #eoc
- #=========================================================
-
-register_crypt_handler(DesCrypt)
-
-#=========================================================
-#extended des crypt
-#=========================================================
-#refs -
-# http://fuse4bsd.creo.hu/localcgi/man-cgi.cgi?crypt+3
-# http://search.cpan.org/dist/Authen-Passphrase/lib/Authen/Passphrase/DESCrypt.pm
-
-class ExtDesCrypt(ExtCryptHandler):
- """Extended BSDi DES Crypt
-
- this algorithm was used on some systems
- during the time between the original crypt()
- and the development of md5-crypt and the modular crypt format.
-
- thus, it doesn't follow the normal format,
- but it does enhance the crypt algorithm to include
- all chars, and adds a rounds parameter.
- """
-
- #=========================================================
- #crypt information
- #=========================================================
- name = "ext-des-crypt"
- #stats: 66 bit checksum, 24 bit salt
-
- setting_kwds = ("salt", "rounds")
-
- salt_chars = 4
-
- #NOTE: this has variable rounds, but it's so old we just max them out by default
- #if this is ever used, since it's so weak to begin with
- default_rounds = 1000
- default_rounds_range = 64
- min_rounds = 25
- max_rounds = 4095
-
- #=========================================================
- #helpers
- #=========================================================
-
- #FORMAT: 2 chars of H64-encoded salt + 11 chars of H64-encoded checksum
- _pat = re.compile(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 parse(cls, hash):
- if not hash:
- raise ValueError, "no hash specified"
- m = cls._pat.match(hash)
- if not m:
- raise ValueError, "not a ext-des-crypt hash"
- return dict(
- rounds=b64_decode_int24(m.group("rounds")),
- salt=m.group("salt"),
- checksum=m.group("chk")
- )
-
- @classmethod
- def render(cls, rounds, salt, checksum=None):
- if rounds < 0:
- raise ValueError, "invalid rounds"
- if len(salt) != 4:
- raise ValueError, "invalid salt"
- if checksum and len(checksum) != 11:
- raise ValueError, "invalid checksum"
- return "_%s%s%s" % (b64_encode_int24(rounds), salt, checksum or '')
-
- #=========================================================
- #frontend
- #=========================================================
- @classmethod
- def identify(cls, hash):
- return bool(hash and cls._pat.match(hash))
-
- @classmethod
- def genconfig(cls, salt=None, rounds=None):
- salt = cls._norm_salt(salt)
- rounds = cls._norm_rounds(rounds)
- return cls.render(salt, rounds)
-
- @classmethod
- def genhash(cls, secret, config):
- info = cls._parse_norm_config(config)
- chk = raw_ext_crypt(secret, info['salt'], info['rounds'])
- return cls.render(rounds, salt, chk)
-
- #=========================================================
- #eoc
- #=========================================================
-
-register_crypt_handler(ExtDesCrypt)
-#=========================================================
-# eof
-#=========================================================
diff --git a/passlib/unix/sha_crypt.py b/passlib/unix/sha_crypt.py
deleted file mode 100644
index ff01599..0000000
--- a/passlib/unix/sha_crypt.py
+++ /dev/null
@@ -1,476 +0,0 @@
-"""passlib.unix.sha_crypt - implements SHA-256-Crypt & SHA-512-Crypt
-
-This implementation is based on Ulrich Drepper's
-``sha-crypt specification <http://www.akkadia.org/drepper/sha-crypt.txt>``.
-It should be byte-compatible with unix shadow hashes beginning with ``$5$`` and ``$6%``.
-
-About
-=====
-This implementation is based on Ulrich Drepper's
-``sha-crypt specification <http://www.akkadia.org/drepper/sha-crypt.txt>``.
-It should be byte-compatible with unix shadow hashes beginning with ``$5$`` and ``$6%``.
-
-This module is not intended to be used directly,
-but merely as a backend for :mod:`passlib.unix.sha_crypt`
-when native sha crypt support is not available.
-
-Deviations from the Specification
-=================================
-
-Unicode
--------
-The sha-crypt specification makes no statement regarding
-the unicode support, it merely takes in a series of bytes.
-
-In order to support non-ascii passwords and :class:`unicode` class,
-this implementation makes the arbitrary decision to encode all unicode passwords
-to ``utf-8`` before passing it into the encryption function.
-
-Salt Length
------------
-The sha-crypt specification allows salt strings of length 0-16 inclusive.
-However, most implementations (including this one) will only
-generate salts of length 16, though they allow the full range.
-
-Salt Characters
----------------
-The charset used by salt strings is poorly defined for sha-crypt.
-
-The sha-crypt spec does not make any statements about the allowable
-salt charset, one way or the other. Furthermore, the reference implementation
-within the spec, and linux implementation, cheerfully allow
-all 8-bit values besides ``\x00`` and ``$``, and excluding
-those not by choice, but due to implementation details.
-Thus the argument could be made that all other characters should be allowed.
-
-However, allowing the characters ``:`` and ``\n`` would cause
-problems for the most common application of this algorithm,
-storage in ``/etc/shadow``. As well, the most unix shadow suites
-only generate salts using the chars ``./0-9A-Za-z``.
-
-Thus, as a compromise, this implementation of sha-crypt
-will allow all salt characters except for ``\x00\n:$``,
-in order to support as much of the specification as feasible;
-but it will only generate salts using the chars ``./0-9A-Za-z``,
-in order to remain compatible with the majority of hashes
-out there, in case other tools have made different assumptions.
-"""
-#=========================================================
-#imports
-#=========================================================
-from __future__ import with_statement
-#core
-import re
-import hashlib
-import logging; log = logging.getLogger(__name__)
-import time
-import os
-#site
-#libs
-from passlib.handler import ExtCryptHandler, register_crypt_handler
-from passlib.utils import abstract_class_method, \
- h64_encode_3_offsets, h64_encode_2_offsets, h64_encode_1_offset
-#pkg
-#local
-__all__ = [
- 'Sha256Crypt',
- 'Sha512Crypt',
-]
-
-#=========================================================
-#pure-python backend
-#=========================================================
-def raw_sha_crypt(secret, salt, rounds, hash):
- """perform raw sha crypt
-
- :arg secret: password to encode (if unicode, encoded to utf-8)
- :arg salt: salt string to use (required)
- :arg rounds: int rounds
- :arg hash: hash constructor function for 256/512 variant
-
- :returns:
- Returns tuple of ``(unencoded checksum, normalized salt, normalized rounds)``.
-
- """
- #validate secret
- if isinstance(secret, unicode):
- secret = secret.encode("utf-8")
-
- #validate rounds
- if rounds < 1000:
- rounds = 1000
- if rounds > 999999999:
- rounds = 999999999
-
- #validate salt
- if any(c in salt for c in '\x00$'):
- raise ValueError, "invalid chars in salt"
- if len(salt) > 16:
- salt = salt[:16]
-
- #init helpers
- def extend(source, size_ref):
- "helper which repeats <source> digest string until it's the same length as <size_ref> string"
- assert len(source) == chunk_size
- size = len(size_ref)
- return source * int(size/chunk_size) + source[:size % chunk_size]
-
- #calc digest B
- b = hash(secret)
- chunk_size = b.digest_size #grab this once hash is created
- b.update(salt)
- a = b.copy() #make a copy to save a little time later
- b.update(secret)
- b_result = b.digest()
- b_extend = extend(b_result, secret)
-
- #begin digest A
- #a = hash(secret) <- performed above
- #a.update(salt) <- performed above
- a.update(b_extend)
-
- #for each bit in slen, add B or SECRET
- value = len(secret)
- while value > 0:
- if value % 2:
- a.update(b_result)
- else:
- a.update(secret)
- value >>= 1
-
- #finish A
- a_result = a.digest()
-
- #calc DP - hash of password, extended to size of password
- dp = hash(secret * len(secret))
- dp_result = extend(dp.digest(), secret)
-
- #calc DS - hash of salt, extended to size of salt
- ds = hash(salt * (16+ord(a_result[0])))
- ds_result = extend(ds.digest(), salt) #aka 'S'
-
- #
- #calc digest C
- #NOTE: this has been contorted a little to allow pre-computing
- #some of the hashes. the original algorithm was that
- #each round generates digest composed of:
- # if round%2>0 => dp else lr
- # if round%3>0 => ds
- # if round%7>0 => dp
- # if round%2>0 => lr else dp
- #where lr is digest of the last round's hash (initially = a_result)
- #
-
- #pre-calculate some digests to speed up odd rounds
- dp_hash = hash(dp_result).copy
- dp_ds_hash = hash(dp_result + ds_result).copy
- dp_dp_hash = hash(dp_result * 2).copy
- dp_ds_dp_hash = hash(dp_result + ds_result + dp_result).copy
-
- #pre-calculate some strings to speed up even rounds
- ds_dp_result = ds_result + dp_result
- dp_dp_result = dp_result * 2
- ds_dp_dp_result = ds_result + dp_dp_result
-
- #run through rounds
- last_result = a_result
- i = 0
- while i < rounds:
- if i % 2:
- if i % 3:
- if i % 7:
- c = dp_ds_dp_hash()
- else:
- c = dp_ds_hash()
- elif i % 7:
- c = dp_dp_hash()
- else:
- c = dp_hash()
- c.update(last_result)
- else:
- c = hash(last_result)
- if i % 3:
- if i % 7:
- c.update(ds_dp_dp_result)
- else:
- c.update(ds_dp_result)
- elif i % 7:
- c.update(dp_dp_result)
- else:
- c.update(dp_result)
- last_result = c.digest()
- i += 1
-
- #return unencoded result, along w/ normalized config values
- return last_result, salt, rounds
-
-def raw_sha256_crypt(secret, salt, rounds):
- "perform raw sha256-crypt; returns encoded checksum, normalized salt & rounds"
- #run common crypt routine
- result, salt, rounds = raw_sha_crypt(secret, salt, rounds, hashlib.sha256)
-
- #encode result
- out = ''
- a, b, c = 0, 10, 20
- while a < 30:
- out += h64_encode_3_offsets(result, c, b, a)
- a, b, c = c+1, a+1, b+1
- assert a == 30, "loop went to far: %r" % (a,)
- out += h64_encode_2_offsets(result, 30, 31)
- assert len(out) == 43, "wrong length: %r" % (out,)
- return out, salt, rounds
-
-def raw_sha512_crypt(secret, salt, rounds):
- "perform raw sha512-crypt; returns encoded checksum, normalized salt & rounds"
- #run common crypt routine
- result, salt, rounds = raw_sha_crypt(secret, salt, rounds, hashlib.sha512)
-
- #encode result
- out = ''
- a, b, c = 0, 21, 42
- while c < 63:
- out += h64_encode_3_offsets(result, c, b, a)
- a, b, c = b+1, c+1, a+1
- assert c == 63, "loop to far: %r" % (c,)
- out += h64_encode_1_offset(result, 63)
- assert len(out) == 86, "wrong length: %r" % (out,)
- return out, salt, rounds
-
-#=========================================================
-#choose backend
-#=========================================================
-
-#fallback to default backend (defined above)
-backend = "builtin"
-
-#check if stdlib crypt is available, and if so, if OS supports $5$ and $6$
-#XXX: is this test expensive enough it should be delayed
-#until sha-crypt is requested?
-
-try:
- from crypt import crypt
-except ImportError:
- crypt = None
-else:
- if (
- crypt("test", "$5$rounds=1000$test") == "$5$rounds=1000$test$QmQADEXMG8POI5WDsaeho0P36yK3Tcrgboabng6bkb/"
- and
- crypt("test", "$6$rounds=1000$test") == "$6$rounds=1000$test$2M/Lx6MtobqjLjobw0Wmo4Q5OFx5nVLJvmgseatA6oMnyWeBdRDx4DU.1H3eGmse6pgsOgDisWBGI5c7TZauS0"
- ):
- backend = "stdlib"
- else:
- crypt = None
-
-#=========================================================
-#ids 5,6 -- sha
-#algorithm defined on this page:
-# http://people.redhat.com/drepper/SHA-crypt.txt
-#=========================================================
-class _ShaCrypt(ExtCryptHandler):
- "common code for used by Sha(256|512)Crypt Classes"
- #=========================================================
- #crypt info
- #=========================================================
-
- #name - provided by subclass
- setting_kwds = ("salt", "rounds")
-
- min_salt_chars = 0
- salt_chars = 16
-
- default_rounds = 40000
- min_rounds = 1000
- max_rounds = 999999999
-
- #=========================================================
- #backend backend
- #=========================================================
- _pat = None #regexp for hash string - provided by subclass
- _ident = None #identifier hash string - provided by subclass
- _raw_crypt = None #corresponding crypt func from builtin backend - provided by subclass
-
- @classmethod
- def _validate_salt_chars(cls, salt):
- #see documentation in _sha_crypt with regards to why we allow
- #all chars except the following...
- if any(c in salt for c in '\x00\n:$'):
- raise ValueError, "invalid %s salt: '\\x00', '\\n', ':', and '$' chars forbidden" % (cls.name,)
- return salt
-
- #TODO: merge this into parse()
- @classmethod
- def parse_config(cls, config):
- "parse partial hash containing just salt+rounds, with salt potentially too large"
- if not config:
- raise ValueError, "invalid sha hash/salt"
- m = re.search(r"""
- ^
- \$""" + cls._ident + r"""
- (\$rounds=(?P<rounds>\d+))?
- \$(?P<salt>[^:$]*)
- $
- """, config, re.X)
- if not m:
- raise ValueError, "invalid sha hash/salt"
- rounds, salt = m.group("rounds", "salt")
- return dict(
- implicit_rounds = not rounds,
- rounds = int(rounds) if rounds else 5000,
- salt = salt,
- )
-
- #=========================================================
- #frontend helpers
- #=========================================================
- @classmethod
- def identify(cls, hash):
- "identify bcrypt hash"
- return bool(hash and cls._pat.match(hash))
-
- @classmethod
- def parse(cls, hash):
- "parse bcrypt hash"
- if not hash:
- raise ValueError, "invalid sha hash/salt"
- m = cls._pat.match(hash)
- if not m:
- raise ValueError, "invalid sha hash/salt"
- rounds, salt, chk = m.group("rounds", "salt", "chk")
- return dict(
- implicit_rounds = not rounds,
- rounds = int(rounds) if rounds else 5000,
- salt=salt,
- checksum=chk,
- )
-
- @classmethod
- def render(cls, rounds, salt, checksum=None, implicit_rounds=True):
- assert '$' not in salt
- if rounds == 5000 and implicit_rounds:
- out = "$%s$%s" % (cls._ident, salt)
- else:
- out = "$%s$rounds=%d$%s" % (cls._ident, rounds, salt)
- if checksum:
- out += "$" + checksum
- return out
-
- @classmethod
- def genconfig(cls, salt=None, rounds=None, implicit_rounds=True):
- salt = cls._norm_salt(salt)
- rounds = cls._norm_rounds(rounds)
- return cls.render(rounds, salt, None, implicit_rounds)
-
- @classmethod
- def genhash(cls, secret, config):
- config = cls._norm_config(config)
- if crypt:
- #using system's crypt routine.
- if isinstance(secret, unicode):
- secret = secret.encode("utf-8")
- return crypt(secret, config)
- else:
- #using builtin routine
- info = cls.parse(config)
- checksum, salt, rounds = cls._raw_crypt(secret, info['salt'], info['rounds'])
- return cls.render(rounds, salt, checksum, info['implicit_rounds'])
-
- #=========================================================
- #eoc
- #=========================================================
-
-class Sha256Crypt(_ShaCrypt):
- """This class implements the SHA-256 Crypt Algorithm,
- according to the specification at `<http://people.redhat.com/drepper/SHA-crypt.txt>`_.
- It should be byte-compatible with unix shadow hashes beginning with ``$5$``.
-
- See Sha512Crypt for usage examples and details.
- """
- #=========================================================
- #algorithm info
- #=========================================================
- name='sha256-crypt'
- #stats: 256 bit checksum, 96 bit salt, 1000..10e8-1 rounds
-
- #=========================================================
- #backend
- #=========================================================
- _ident = '5'
-
- _pat = re.compile(r"""
- ^
- \$(?P<ident>5)
- (\$rounds=(?P<rounds>\d+))?
- \$(?P<salt>[^:$]{0,16})
- \$(?P<chk>[A-Za-z0-9./]{43})
- $
- """, re.X)
-
- _raw_crypt = raw_sha256_crypt
-
- #=========================================================
- #eof
- #=========================================================
-
-register_crypt_handler(Sha256Crypt)
-
-class Sha512Crypt(_ShaCrypt):
- """This class implements the SHA-512 Crypt Algorithm,
- according to the specification at `http://people.redhat.com/drepper/SHA-crypt.txt`_.
- It should be byte-compatible with unix shadow hashes beginning with ``$6$``.
-
- This implementation is based on a pure-python translation
- of the original specification.
-
- .. note::
- This is *not* just the raw SHA-512 hash of the password,
- which is sometimes incorrectly referred to as sha512-crypt.
- This is a variable-round descendant of md5-crypt,
- and is comparable in strength to bcrypt.
-
- Usage Example::
-
- >>> from passlib import Sha512Crypt
- >>> crypt = Sha512Crypt()
- >>> #to encrypt a new secret with this algorithm
- >>> hash = crypt.encrypt("forget me not")
- >>> hash
- '$6$rounds=11949$KkBupsnnII6YXqgT$O8qAEcEgDyJlMC4UB3buST8vE1PsPPABA.0lQIUARTNnlLPZyBRVXAvqqynVByGRLTRMIorkcR0bsVQS5i3Xw1'
- >>> #to verify an existing secret
- >>> crypt.verify("forget me not", hash)
- True
- >>> crypt.verify("i forgot it", hash)
- False
-
- .. automethod:: encrypt
- """
- #=========================================================
- #crypt info
- #=========================================================
- name='sha512-crypt'
- #stats: 512 bit checksum, 96 bit salt, 1000..10e8-1 rounds
-
- #=========================================================
- #backend
- #=========================================================
- _ident = '6'
-
- _pat = re.compile(r"""
- ^
- \$(?P<ident>6)
- (\$rounds=(?P<rounds>\d+))?
- \$(?P<salt>[^:$]{0,16})
- \$(?P<chk>[A-Za-z0-9./]{86})
- $
- """, re.X)
-
- _raw_crypt = raw_sha512_crypt
-
- #=========================================================
- #eof
- #=========================================================
-
-register_crypt_handler(Sha512Crypt)
-
-#=========================================================
-# eof
-#=========================================================
diff --git a/passlib/utils/__init__.py b/passlib/utils/__init__.py
index 3b93df1..728f814 100644
--- a/passlib/utils/__init__.py
+++ b/passlib/utils/__init__.py
@@ -11,8 +11,10 @@ import os
import sys
import random
import time
+from warnings import warn
#site
#pkg
+import passlib.utils.h64
#local
__all__ = [
#decorators
@@ -25,9 +27,10 @@ __all__ = [
"list_to_bytes",
"xor_bytes",
- #hash64 encoding
- 'generate_h64_salt',
- 'validate_h64_salt',
+ #misc helpers
+ 'gen_salt',
+ 'norm_salt',
+ 'norm_rounds',
]
#=================================================================================
#decorators
@@ -312,73 +315,102 @@ def getrandstr(rng, alphabet, count):
return buf.getvalue()
#=================================================================================
-# "hash64" encoding helpers
-#
-# many of the password hash algorithms in this module
-# use a encoding that maps chunks of 3 bytes ->
-# chunks of 4 characters, in a manner similar (but not compatible with) base64.
-#
-# this encoding system appears to have originated with unix-crypt,
-# but is used by md5-crypt, sha-xxx-crypt, and others.
-# this encoded is referred to (within passlib) as hash64 encoding,
-# due to it's use of a strict set of 64 ascii characters.
-#
-# notably, bcrypt uses the same scheme, but with a different
-# ordering of the characters. bcrypt hashes cannot be decoded properly
-# with the following rountines (though h64_gensalt & h64_validate work fine)
-#
+#misc helpers
#=================================================================================
+def norm_rounds(rounds, default_rounds, min_rounds, max_rounds, name="this crypt"):
+ """helper routine for normalizing rounds
+
+ * falls back to :attr:`default_rounds`
+ * raises ValueError if no fallback
+ * clips to min_rounds / max_rounds
+ * issues warnings if rounds exists min/max
+
+ :returns: normalized rounds value
+ """
+ if rounds is None:
+ rounds = default_rounds
+ if rounds is None:
+ raise ValueError, "rounds must be specified explicitly"
+
+ if rounds > max_rounds:
+ warn("%s algorithm does not allow more than %d rounds: %d" % (name, max_rounds, rounds))
+ rounds = max_rounds
+
+ if rounds < min_rounds:
+ warn("%s algorithm does not allow less than %d rounds: %d" % (name, min_rounds, rounds))
+ rounds = min_rounds
+
+ return rounds
+
+def gen_salt(count, charset=h64.CHARS):
+ global rng
+ return getrandstr(rng, charset, count)
+
+def norm_salt(salt, min_chars, max_chars=None, charset=h64.CHARS, gen_charset=None, name="specified"):
+ """helper to normalize & validate user-provided salt string
+
+ required salt_charset & salt_chars attrs to be filled in,
+ along with optional min_salt_chars attr (defaults to salt_chars).
+
+ * generates salt if none provided
+ * clips salt to maximum length of salt_chars
+
+ :arg salt: user-provided salt
+ :arg min_chars: minimum number of chars in salt
+ :arg max_chars: maximum number of chars in salt (if omitted, same as min_chars)
+ :param charset: character set that salt MUST be subset of
+ :param gen_charset: optional character set to restrict to when generating new salts (defaults to charset)
+ :param name: optional name of handler, for inserting into error messages
+
+ :raises ValueError:
+ * if salt contains chars that aren't in salt_charset.
+ * if salt contains less than min_salt_chars characters.
+
+ :returns:
+ resulting or generated salt
+ """
+ #generate one if needed
+ if salt is None:
+ return gen_salt(max_chars or min_chars, gen_charset or charset)
+
+ #check character set
+ for c in salt:
+ if c not in charset:
+ raise ValueError, "invalid character in %s salt: %r" % (name, c)
+
+ #check min size
+ if len(salt) < min_chars:
+ raise ValueError, "%s salt must be at least %d chars" % (name, min_chars)
+
+ if max_chars is None:
+ max_chars = min_chars
+ if len(salt) > max_chars:
+ #automatically clip things to specified number of chars
+ return salt[:max_chars]
+ else:
+ return salt
+
+#=================================================================================
+#errors
+#=================================================================================
+
+###NOTE: not all handlers will raise these errors,
+### the only thing currently guaranteed is that they
+### will
+##
+##class HandlerError(ValueError):
+## "helper class for various errors used by some CryptHandlers"
+## message = None
+##
+## def __init__(self, msg=None):
+## ValueError.__init__(self, msg or self.message)
+##
+##class NoChecksumError(HandlerError):
+## "helper raised by CryptHandler.verify() when config string passed in instead of hash"
+## #helper for common message raised by handlers
+##
+## message = "hash lacks checksum (did you pass a config string into verify?)"
-H64_CHARS = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
-
-def generate_h64_salt(count):
- "return base64 salt containing specified number of characters"
- return getrandstr(rng, H64_CHARS, count)
-
-def validate_h64_salt(value, count):
- "validate base64 encoded salt is of right size & charset"
- if not value:
- raise ValueError, "no salt specified"
- if len(value) != count:
- raise ValueError, "salt must have %d chars: %r" % (count, value)
- for c in value:
- if c not in H64_CHARS:
- raise ValueError, "invalid %r character in salt: %r" % (c, value)
- return True
-
-def h64_encode_3_offsets(buffer, o1, o2, o3):
- "do hash64 encode of three bytes at specified offsets in buffer; returns 4 chars"
- #how 4 char output corresponds to 3 byte input:
- #
- #1st character: the six low bits of the first byte (0x3F)
- #
- #2nd character: four low bits from the second byte (0x0F) shift left 2
- # the two high bits of the first byte (0xC0) shift right 6
- #
- #3rd character: the two low bits from the third byte (0x03) shift left 4
- # the four high bits from the second byte (0xF0) shift right 4
- #
- #4th character: the six high bits from the third byte (0xFC) shift right 2
- v1 = ord(buffer[o1])
- v2 = ord(buffer[o2])
- v3 = ord(buffer[o3])
- return H64_CHARS[v1&0x3F] + \
- H64_CHARS[((v2&0x0F)<<2) + (v1>>6)] + \
- H64_CHARS[((v3&0x03)<<4) + (v2>>4)] + \
- H64_CHARS[v3>>2]
-
-def h64_encode_2_offsets(buffer, o1, o2):
- "do hash64 encode of two bytes at specified offsets in buffer; 2 missing msg set null; returns 3 chars"
- v1 = ord(buffer[o1])
- v2 = ord(buffer[o2])
- return H64_CHARS[v1&0x3F] + \
- H64_CHARS[((v2&0x0F)<<2) + (v1>>6)] + \
- H64_CHARS[(v2>>4)]
-
-def h64_encode_1_offset(buffer, o1):
- "do hash64 encode of single byte at specified offset in buffer; 4 missing msb set null; returns 2 chars"
- v1 = ord(buffer[o1])
- return H64_CHARS[v1&0x3F] + H64_CHARS[v1>>6]
#=================================================================================
#eof
diff --git a/passlib/utils/_slow_des_crypt.py b/passlib/utils/_slow_des_crypt.py
deleted file mode 100644
index 08e2960..0000000
--- a/passlib/utils/_slow_des_crypt.py
+++ /dev/null
@@ -1,167 +0,0 @@
-"""passlib._slow_unix_crypt -- fallback pure-python unix crypt() implementation
-
-This module is mainly meant as a fallback when stdlib does not supply a ``crypt`` implementation,
-such as on windows systems. As such, it attempts to have a public interface
-which is compatible with stdlib, so it can be used as a drop-in replacement.
-"""
-#=========================================================
-#imports
-#=========================================================
-#pkg
-from passlib.utils import H64_CHARS
-from passlib.utils.des import mdes_encrypt_int_block
-#local
-__all__ = [
- "crypt",
- "raw_ext_crypt",
- "ext_crypt",
-]
-
-#=========================================================
-#crypt-style base64 encoding / decoding
-#=========================================================
-
-#base64 char sequence
-b64_encode_6bit = H64_CHARS.__getitem__ # int -> char
-
-#inverse map (char->value)
-CHARIDX = dict( (c,i) for i,c in enumerate(H64_CHARS))
-b64_decode_6bit = CHARIDX.__getitem__ # char -> int
-
-##def b64_to_int(value):
-## "decode hash-64 format used by crypt into integer"
-## #FORMAT: little-endian, each char contributes 6 bits,
-## # char value = index in H64_CHARS string
-## try:
-## out = 0
-## for c in reversed(value):
-## out = (out<<6) + b64_decode_6bit(c)
-## return out
-## except KeyError:
-## raise ValueError, "invalid character in string"
-
-def b64_decode_int12(value):
- "decode 2 chars of hash-64 format used by crypt, returning 12-bit integer"
- try:
- return (b64_decode_6bit(value[1])<<6)+b64_decode_6bit(value[0])
- except KeyError:
- raise ValueError, "invalid character"
-
-def b64_decode_int24(value):
- "decode 4 chars of hash-64 format used by crypt, returning 24-bit integer"
- try:
- return b64_decode_6bit(value[0]) +\
- (b64_decode_6bit(value[1])<<6)+\
- (b64_decode_6bit(value[3])<<18)+\
- (b64_decode_6bit(value[2])<<12)
- except KeyError:
- raise ValueError, "invalid character"
-
-def b64_encode_int24(value):
- "decode 2 chars of hash-64 format used by crypt, returning 12-bit integer"
- return b64_encode_6bit(value & 0x3f) + \
- b64_encode_6bit((value>>6) & 0x3f) + \
- b64_encode_6bit((value>>12) & 0x3f) + \
- b64_encode_6bit((value>>18) & 0x3f)
-
-def b64_encode_int64(value):
- "encode 64-bit integer to hash-64 format used by crypt, returning 11 chars"
- out = [None] * 10 + [ b64_encode_6bit((value<<2)&0x3f) ]
- value >>= 4
- for i in RR9_1:
- out[i] = b64_encode_6bit(value&0x3f)
- value >>= 6
- return "".join(out)
-
-#=========================================================
-#crypt frontend
-#=========================================================
-def _crypt_secret_to_key(secret):
- key_value = 0
- for i, c in enumerate(secret[:8]):
- key_value |= (ord(c)&0x7f) << (57-8*i)
- return key_value
-
-def crypt(secret, config):
- "encrypt string using unix-crypt (des) algorithm"
- #parse config
- if not config or len(config) < 2:
- raise ValueError, "invalid salt"
-
- salt = config[:2]
- try:
- salt_value = b64_decode_int12(salt)
- except ValueError:
- raise ValueError, "invalid chars in salt"
- #FIXME: ^ this will throws error if bad salt chars are used
- # whereas linux crypt does something (inexplicable) with it
-
- #validate secret
- if '\x00' in secret:
- #builtin linux crypt doesn't like this, so we don't either
- #XXX: would make more sense to raise ValueError, but want to be compatible w/ stdlib crypt
- raise ValueError, "secret must be string without null bytes"
-
- #XXX: doesn't match stdlib, but just to useful to not add in
- if isinstance(secret, unicode):
- secret = secret.encode("utf-8")
-
- #convert secret string into an integer
- key_value = _crypt_secret_to_key(secret)
-
- #run data through des using input of 0
- result = mdes_encrypt_int_rounds(key_value, 0, salt=salt_value, rounds=25)
-
- #run h64 encode on result
- return salt + b64_encode_int64(result)
-
-#=========================================================
-#ext crypt frontend
-#=========================================================
-def raw_ext_crypt(secret, salt, rounds):
- "ext_crypt() helper which returns checksum only"
-
- #decode salt
- try:
- salt_value = b64_decode_int24(salt)
- except ValueError:
- raise ValueError, "invalid salt"
-
- #validate secret
- if '\x00' in secret:
- #builtin linux crypt doesn't like this, so we don't either
- #XXX: would make more sense to raise ValueError, but want to be compatible w/ stdlib crypt
- raise ValueError, "secret must be string without null bytes"
-
- #XXX: doesn't match stdlib, but just to useful to not add in
- if isinstance(secret, unicode):
- secret = secret.encode("utf-8")
-
- #convert secret string into an integer
- key_value = _crypt_secret_to_key(secret)
- while len(secret) > 8:
- secret = secret[8:]
- key_value = mdes_encrypt_rounds(key_value, key_value, salt=0, rounds=1)
- for i,c in enumerate(secret[:8]):
- key_value ^= (ord(c)&0x7f)<<(57-8*i)
-
- #run data through des using input of 0
- result = mdes_encrypt_int_rounds(key_value, 0, salt=salt_value, rounds=rounds)
-
- #run h64 encode on result
- return b64_encode_int64(result)
-
-def ext_crypt(secret, config):
- "perform extended unix crypt (BSDi's 3DES modification of crypt)"
- if not config or len(config) < 5 or not config.startswith("_"):
- raise ValueError, "invalid config string"
- try:
- rounds = b64_decode_int24(config[1:5])
- except ValueError:
- raise ValueError, "invalid rounds specification"
- salt = config[5:9]
- return config[:9] + raw_ext_crypt(secret, salt, rounds)
-
-#=========================================================
-#eof
-#=========================================================
diff --git a/passlib/utils/des.py b/passlib/utils/des.py
index ca27f10..0851bb3 100644
--- a/passlib/utils/des.py
+++ b/passlib/utils/des.py
@@ -9,6 +9,7 @@ such as ``lmhash`` and ``des-crypt``.
.. function:: expand_des_key
.. function:: des_encrypt_block
.. function:: mdes_encrypt_int_block
+
"""
"""
@@ -45,6 +46,9 @@ The copyright & license for that source is as follows::
@version $Id: UnixCrypt2.txt,v 1.1.1.1 2005/09/13 22:20:13 christos Exp $
@author Greg Wilkins (gregw)
"""
+
+#TODO: could use an accelerated C version of this module to speed up lmhash, des-crypt, and ext-des-crypt
+
#=========================================================
#imports
#=========================================================
diff --git a/passlib/utils/h64.py b/passlib/utils/h64.py
new file mode 100644
index 0000000..9e91cb7
--- /dev/null
+++ b/passlib/utils/h64.py
@@ -0,0 +1,145 @@
+"""passlib.utils.h64 - hash64 encoding helpers
+
+many of the password hash algorithms in passlib
+use a encoding scheme very similar to, but not compatible with,
+the standard base64 encoding scheme. the main differences are that
+it uses ``.`` instead of ``+``, and assigns the
+characters *completely* different numeric values.
+
+this encoding system appears to have originated with des-crypt hash,
+but is used by md5-crypt, sha-256-crypt, and others.
+within passlib, this encoding is referred as ``hash64`` encoding,
+and this module contains various utilities functions for encoding
+and decoding strings in that format.
+
+.. note::
+ It may *look* like bcrypt uses this scheme,
+ when in fact bcrypt uses the standard base64 encoding scheme,
+ but with ``+`` replaced with ``.``.
+"""
+#=================================================================================
+#imports
+#=================================================================================
+#core
+import logging; log = logging.getLogger(__name__)
+#site
+#pkg
+#local
+__all__ = [
+ "CHARS",
+
+ "decode_6bit", "encode_6bit",
+
+ "encode_3_offsets",
+ "encode_2_offsets",
+ "encode_1_offset",
+
+ "decode_int12",
+ "decode_int24", "encode_int24",
+ "encode_int64",
+
+]
+
+#=================================================================================
+#6 bit value <-> char mapping
+#=================================================================================
+CHARS = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
+
+#base64 char sequence
+encode_6bit = CHARS.__getitem__ # int -> char
+
+#inverse map (char->value)
+_CHARIDX = dict( (c,i) for i,c in enumerate(CHARS))
+decode_6bit = _CHARIDX.__getitem__ # char -> int
+
+#=================================================================================
+#encode offsets from buffer - used by md5_crypt, sha_crypt, et al
+#=================================================================================
+def encode_3_offsets(buffer, o1, o2, o3):
+ "do hash64 encode of three bytes at specified offsets in buffer; returns 4 chars"
+ #how 4 char output corresponds to 3 byte input:
+ #
+ #1st character: the six low bits of the first byte (0x3F)
+ #
+ #2nd character: four low bits from the second byte (0x0F) shift left 2
+ # the two high bits of the first byte (0xC0) shift right 6
+ #
+ #3rd character: the two low bits from the third byte (0x03) shift left 4
+ # the four high bits from the second byte (0xF0) shift right 4
+ #
+ #4th character: the six high bits from the third byte (0xFC) shift right 2
+ v1 = ord(buffer[o1])
+ v2 = ord(buffer[o2])
+ v3 = ord(buffer[o3])
+ return encode_6bit(v1&0x3F) + \
+ encode_6bit(((v2&0x0F)<<2) + (v1>>6)) + \
+ encode_6bit(((v3&0x03)<<4) + (v2>>4)) + \
+ encode_6bit(v3>>2)
+
+def encode_2_offsets(buffer, o1, o2):
+ "do hash64 encode of two bytes at specified offsets in buffer; 2 missing msg set null; returns 3 chars"
+ v1 = ord(buffer[o1])
+ v2 = ord(buffer[o2])
+ return encode_6bit(v1&0x3F) + \
+ encode_6bit(((v2&0x0F)<<2) + (v1>>6)) + \
+ encode_6bit((v2>>4))
+
+def encode_1_offset(buffer, o1):
+ "do hash64 encode of single byte at specified offset in buffer; 4 missing msb set null; returns 2 chars"
+ v1 = ord(buffer[o1])
+ return encode_6bit(v1&0x3F) + encode_6bit(v1>>6)
+
+#=================================================================================
+# int <-> b64 string, used by des_crypt, ext_des_crypt
+#=================================================================================
+
+##def decode_int(value):
+## "decode hash-64 format used by crypt into integer"
+## #FORMAT: little-endian, each char contributes 6 bits,
+## # char value = index in H64_CHARS string
+## try:
+## out = 0
+## for c in reversed(value):
+## out = (out<<6) + b64_decode_6bit(c)
+## return out
+## except KeyError:
+## raise ValueError, "invalid character in string"
+
+def decode_int12(value):
+ "decode 2 chars of hash-64 format used by crypt, returning 12-bit integer"
+ try:
+ return (decode_6bit(value[1])<<6)+decode_6bit(value[0])
+ except KeyError:
+ raise ValueError, "invalid character"
+
+def decode_int24(value):
+ "decode 4 chars of hash-64 format used by crypt, returning 24-bit integer"
+ try:
+ return decode_6bit(value[0]) +\
+ (decode_6bit(value[1])<<6)+\
+ (decode_6bit(value[3])<<18)+\
+ (decode_6bit(value[2])<<12)
+ except KeyError:
+ raise ValueError, "invalid character"
+
+def encode_int24(value):
+ "decode 2 chars of hash-64 format used by crypt, returning 12-bit integer"
+ return encode_6bit(value & 0x3f) + \
+ encode_6bit((value>>6) & 0x3f) + \
+ encode_6bit((value>>12) & 0x3f) + \
+ encode_6bit((value>>18) & 0x3f)
+
+_RR9_1 = range(9,-1,-1)
+
+def encode_int64(value):
+ "encode 64-bit integer to hash-64 format used by crypt, returning 11 chars"
+ out = [None] * 10 + [ encode_6bit((value<<2)&0x3f) ]
+ value >>= 4
+ for i in _RR9_1:
+ out[i] = encode_6bit(value&0x3f)
+ value >>= 6
+ return "".join(out)
+
+#=================================================================================
+#eof
+#=================================================================================
diff --git a/passlib/win32.py b/passlib/win32.py
new file mode 100644
index 0000000..9c90b59
--- /dev/null
+++ b/passlib/win32.py
@@ -0,0 +1,54 @@
+"""passlib.win32 - MS Windows support
+
+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:: lmhash
+.. autofunction:: nthash
+"""
+#=========================================================
+#imports
+#=========================================================
+#core
+from binascii import hexlify
+#site
+#pkg
+from passlib.utils.des import des_encrypt_block
+from passlib.utils.md4 import md4
+#local
+__all__ = [
+ "lmhash",
+ "nthash",
+]
+#=========================================================
+#helpers
+#=========================================================
+LM_MAGIC = "KGS!@#$%"
+
+def raw_lmhash(secret, hex=False):
+ "encode password using des-based LMHASH algorithm; returns string of raw bytes"
+ #XXX: encoding should be oem ascii
+ ns = secret.upper()[:14] + "\x00" * (14-len(secret))
+ out = des_encrypt_block(ns[:7], LM_MAGIC) + des_encrypt_block(ns[7:], LM_MAGIC)
+ return hexlify(out) if hex else out
+
+def raw_nthash(secret, hex=False):
+ "encode password using md4-based NTHASH algorithm; returns string of raw bytes"
+ hash = md4(secret.encode("utf-16le"))
+ return hash.hexdigest() if hex else hash.digest()
+
+#=========================================================
+#eoc
+#=========================================================