diff options
| author | Eli Collins <elic@assurancetechnologies.com> | 2011-01-28 06:35:56 +0000 |
|---|---|---|
| committer | Eli Collins <elic@assurancetechnologies.com> | 2011-01-28 06:35:56 +0000 |
| commit | fea8e73c8e9bef3e9423af50c128cb20e7813b09 (patch) | |
| tree | 984691ea244b83ff9cbc8d8692f66f00142f62b9 | |
| parent | bc738f4c6e35a31c9edd5fb54f13773e15978a09 (diff) | |
| download | passlib-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
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 +#========================================================= |
