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 /passlib/handler.py | |
| 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
Diffstat (limited to 'passlib/handler.py')
| -rw-r--r-- | passlib/handler.py | 667 |
1 files changed, 297 insertions, 370 deletions
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 |
