diff options
author | Eli Collins <elic@assurancetechnologies.com> | 2011-01-07 07:37:39 +0000 |
---|---|---|
committer | Eli Collins <elic@assurancetechnologies.com> | 2011-01-07 07:37:39 +0000 |
commit | df53832315758ad9dcecae91fc9a511f3cba575a (patch) | |
tree | a140a764e6c17df24afa34ea7ee6f22a0690bcb0 | |
parent | c9748b14693b2378a20088f46f44b2f56ccfda6f (diff) | |
download | passlib-df53832315758ad9dcecae91fc9a511f3cba575a.tar.gz |
rearranging
===========
* moved hash.py to package
* in process of rearranging so core code is in hash.base,
and separate submodules exist for each of the algorithms.
- sha crypt split out
- unix crypt split out
- similar refactor of UT files
* moved hash64 encoding helpers to passlib.util.H64 class
* reversed order of h64 encoding helpers so offsets match order of output
* added H64 unit tests
unix crypt
----------
(yes this is overkill for legacy crypt)
* cleaned up builtin unix_crypt semantics
* wrapping stdlib crypt to deal w/ border case errors
* added better unix crypt backend test suite
-rw-r--r-- | passlib.komodoproject | 2 | ||||
-rw-r--r-- | passlib/_unix_crypt.py | 35 | ||||
-rw-r--r-- | passlib/hash/__init__.py | 154 | ||||
-rw-r--r-- | passlib/hash/base.py (renamed from passlib/hash.py) | 630 | ||||
-rw-r--r-- | passlib/hash/sha_crypt.py | 355 | ||||
-rw-r--r-- | passlib/hash/unix_crypt.py | 96 | ||||
-rw-r--r-- | passlib/tests/test_crypt_context.py | 601 | ||||
-rw-r--r-- | passlib/tests/test_hash.py | 720 | ||||
-rw-r--r-- | passlib/tests/test_sha_crypt.py | 130 | ||||
-rw-r--r-- | passlib/tests/test_unix_crypt.py | 121 | ||||
-rw-r--r-- | passlib/tests/test_util.py | 49 | ||||
-rw-r--r-- | passlib/tests/utils.py | 18 | ||||
-rw-r--r-- | passlib/util.py | 166 |
13 files changed, 1723 insertions, 1354 deletions
diff --git a/passlib.komodoproject b/passlib.komodoproject index 62697df..d4e8545 100644 --- a/passlib.komodoproject +++ b/passlib.komodoproject @@ -3,7 +3,7 @@ <project id="ab14e891-3bad-4dee-94c1-e8ad8d6ca1a9" kpf_version="5" name="passlib.komodoproject"> <preference-set idref="ab14e891-3bad-4dee-94c1-e8ad8d6ca1a9"> <string relative="path" id="import_dirname"></string> - <string id="import_exclude_matches">dist;build;.*;*.*~;*.bak;*.tmp;CVS;.#*;*.pyo;*.pyc;.svn;*%*;tmp*.html;.DS_Store</string> + <string id="import_exclude_matches">_build;dist;build;.*;*.*~;*.bak;*.tmp;CVS;.#*;*.pyo;*.pyc;.svn;*%*;tmp*.html;.DS_Store</string> <string id="import_include_matches"></string> <boolean id="import_live">1</boolean> </preference-set> diff --git a/passlib/_unix_crypt.py b/passlib/_unix_crypt.py index 9520bd2..05898a9 100644 --- a/passlib/_unix_crypt.py +++ b/passlib/_unix_crypt.py @@ -44,6 +44,8 @@ which is compatible with stdlib, so it can be used as a drop-in replacement. #simple constants #========================================================= +#XXX: can b64_encode / b64_decode be replaced with H64.encode/decode? + #base64 char sequence CHARS = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' b64_encode = CHARS.__getitem__ # int -> char @@ -621,10 +623,26 @@ def crypt(key, salt): if PCXROT is None: load_tables() + #parse key values if '\x00' in key: - #builtin linux crypt doesn't like this, - #so we don't either - raise ValueError, "key must not contain null characters" + #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, "key must be string without null bytes" + + #XXX: doesn't match stdlib, but just to useful to not add in + if isinstance(key, unicode): + key = key.encode("utf-8") + + #parse salt into bytes + if not salt or len(salt) < 2: + raise ValueError, "invalid salt" + sa, sb = salt[0:2] + try: + salt_value = (b64_decode(sb) << 6) + b64_decode(sa) + except KeyError: + raise ValueError, "invalid salt" + #FIXME: ^ this will throws error if bad salt chars are used + # whereas linux crypt does something (inexplicable) with it #convert key string into an integer if len(key) < 8: @@ -637,17 +655,6 @@ def crypt(key, salt): #convert key int -> key schedule key_sched = des_setkey(key_value) - #parse salt into int - if not salt: - sa = sb = '.' #no std behavior in this case - elif len(salt) < 2: - sa = sb = salt #linux's behavior, probably no real standard - else: - sa, sb = salt[0:2] - salt_value = (b64_decode(sb) << 6) + b64_decode(sa) - #FIXME: ^ this will throw a KeyError if bad salt chars are used - # whereas linux crypt does something with it - #run data through des using input of 0 result = des_cipher(0, salt_value, 25, key_sched) diff --git a/passlib/hash/__init__.py b/passlib/hash/__init__.py new file mode 100644 index 0000000..a7b03dc --- /dev/null +++ b/passlib/hash/__init__.py @@ -0,0 +1,154 @@ +from passlib.hash.base import * +from passlib.hash.unix_crypt import * +from passlib.hash.sha_crypt import * + +#========================================================= +#build up the standard context objects +#========================================================= + +#default context for quick use.. recognizes all known algorithms, +# currently uses SHA-512 as default +default_context = CryptContext([ UnixCrypt, Md5Crypt, BCrypt, Sha256Crypt, Sha512Crypt ]) + +def identify(hash, resolve=False): + """Identify algorithm which generated a password hash. + + :arg hash: + The hash string to identify. + :param resolve: + If ``True``, this function will return a :class:`CryptAlgorithm` + instance which can handle the hash. + If ``False`` (the default), then only the name of the hash algorithm + will be returned. + + The following algorithms are currently recognized: + + =================== ================================================ + Name Description + ------------------- ------------------------------------------------ + ``"unix-crypt"`` the historical unix-crypt algorithm + + ``"md5-crypt"`` the md5-crypt algorithm, usually identified + by the prefix ``$1$`` in unix shadow files. + + ``"bcrypt"`` the openbsd blowfish-crypt algorithm, + usually identified by the prefixes ``$2$`` or ``$2a$`` + in unix shadow files. + + ``"sha256-crypt"`` the 256-bit version of the sha-crypt algorithm, + usually identified by the prefix ``$5$`` + in unix shadow files. + + ``"sha512-crypt"`` the 512-bit version of the sha-crypt algorithm, + usually identified by the prefix ``$6$`` + in unix shadow files. + =================== ================================================ + + :returns: + The name of the hash, or ``None`` if the hash could not be identified. + (The return may be altered by the *resolve* keyword). + + .. note:: + This is a convience wrapper for ``pwhash.default_context.identify(hash)``. + """ + return default_context.identify(hash, resolve=resolve) + +def encrypt(secret, hash=None, alg=None, **kwds): + """Encrypt secret using a password hash algorithm. + + :type secret: str + :arg secret: + String containing the secret to encrypt + + :type hash: str|None + :arg hash: + Optional previously existing hash string which + will be used to provide default value for the salt, rounds, + or other algorithm-specific options. + If not specified, algorithm-chosen defaults will be used. + + :type alg: str|None + :param alg: + Optionally specify the name of the algorithm to use. + If no algorithm is specified, an attempt is made + to guess from the hash string. If no hash string + is specified, sha512-crypt will be used. + See :func:`identify` for a list of algorithm names. + + All other keywords are passed on to the specific password algorithm + being used to encrypt the secret. + + :type keep_salt: bool + :param keep_salt: + This option is accepted by all of the builtin algorithms. + + By default, a new salt value generated each time + a secret is encrypted. However, if this keyword + is set to ``True``, and a previous hash string is provided, + the salt from that string will be used instead. + + .. note:: + This is generally only useful when verifying an existing hash + (see :func:`verify`). Other than that, this option should be + avoided, as re-using a salt will needlessly decrease security. + + :type rounds: int + :param rounds: + For the sha256-crypt and sha512-crypt algorithms, + this option lets you specify the number of rounds + of encryption to use. For the bcrypt algorithm, + this option lets you specify the log-base-2 of + the number of rounds of encryption to use. + + For all three of these algorithms, you can either + specify a positive integer, or one of the strings + "fast", "medium", "slow" to choose a preset number + of rounds corresponding to an appropriate level + of encryption. + + :returns: + The secret as encoded by the specified algorithm and options. + """ + return default_context.encrypt(secret, hash=hash, alg=alg, **kwds) + +def verify(secret, hash, alg=None): + """verify a secret against an existing hash. + + This checks if a secret matches against the one stored + inside the specified hash. By default this uses :func:`encrypt` + to re-crypt the secret, and compares it to the provided hash; + though some algorithms may implement this in a more efficient manner. + + :type secret: str + :arg secret: + A string containing the secret to check. + + :type hash: str + :param hash: + A string containing the hash to check against. + + :type alg: str|None + :param alg: + Optionally specify the name of the algorithm to use. + If no algorithm is specified, an attempt is made + to guess from the hash string. If it can't be + identified, a ValueError will be raised. + See :func:`identify` for a list of algorithm names. + + :returns: + ``True`` if the secret matches, otherwise ``False``. + """ + return default_context.verify(secret, hash, alg=alg) + +#some general os-context helpers (these may not match your os policy exactly) +linux_context = CryptContext([ UnixCrypt, Md5Crypt, Sha256Crypt, Sha512Crypt ]) +bsd_context = CryptContext([ UnixCrypt, Md5Crypt, BCrypt ]) + +#some sql db context helpers +mysql40_context = CryptContext([Mysql10Crypt]) +mysql_context = CryptContext([Mysql10Crypt, Mysql41Crypt]) +postgres_context = CryptContext([PostgresMd5Crypt]) + +#========================================================= +#eof +#========================================================= diff --git a/passlib/hash.py b/passlib/hash/base.py index 2a2a3eb..ad50936 100644 --- a/passlib/hash.py +++ b/passlib/hash/base.py @@ -12,7 +12,7 @@ import time import os #site #libs -from passlib.util import classproperty, abstractmethod, is_seq, srandom +from passlib.util import classproperty, abstractmethod, is_seq, srandom, H64 try: #try importing py-bcrypt, it's much faster @@ -28,155 +28,19 @@ __all__ = [ 'CryptAlgorithm', 'UnixCrypt', 'Md5Crypt', - 'Sha256Crypt', - 'Sha512Crypt', 'BCrypt', + 'Mysql10Crypt', + 'Mysql41Crypt', + 'PostgresMd5Crypt', #crypt context 'CryptContext', - 'default_context', - 'linux_context', - 'bsd_context', - - #quick helpers - 'identify_secret', - 'encrypt_secret', - 'verify_secret', - ] #========================================================= #common helper funcs for passwords #========================================================= -#charmap for "hash64" encoding -#most unix hash algorithms use this mapping (though bcrypt put it's numerals at the end) -CHARS = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" -CHARIDX = dict( (c,i) for i,c in enumerate(CHARS)) - -def _enc64(value, offset=0, num=False): - if num: - x, y, z = value[offset], value[offset+1], value[offset+2] - else: - x, y, z = ord(value[offset]), ord(value[offset+1]), ord(value[offset+2]) - #xxxxxx xxyyyy yyyyzz zzzzzz - #aaaaaa bbbbbb cccccc dddddd - a = (x >> 2) # x [8..3] - b = ((x & 0x3) << 4) + (y>>4) # x[2..1] + y [8..5] - c = ((y & 0xf) << 2) + (z>>6) #y[4..1] + d[8..7] - d = z & 0x3f - return CHARS[a] + CHARS[b] + CHARS[c] + CHARS[d] - -def _dec64(value, offset=0, num=False): - a, b, c, d = CHARIDX[value[offset]], CHARIDX[value[offset+1]], \ - CHARIDX[value[offset+2]], CHARIDX[value[offset+3]] - #aaaaaabb bbbbcccc ccdddddd - #xxxxxxxx yyyyyyyy zzzzzzzz - x = (a<<2) + (b >> 4) #a[6..1] + b[6..5] - y = ((b & 0xf) << 4) + (c >> 2) #b[4..1] + c[6..3] - z = ((c & 0x3) << 6) + d #c[2..1] + d[6..1] - if num: - return x, y, z - return chr(x) + chr(y) + chr(z) - -def h64_encode(value, pad=False, num=False): - "encode string of bytes into hash64 format" - if num: - value = list(value) - #pad value to align w/ 3 byte chunks - x = len(value) % 3 - if x == 2: - if num: - value += [0] - else: - value += "\x00" - p = 1 - elif x == 1: - if num: - value += [0, 0] - else: - value += "\x00\x00" - p = 2 - else: - p = 0 - assert len(value) % 3 == 0 - out = "".join( _enc64(value, offset, num=num) for offset in xrange(0, len(value), 3)) - assert len(out) % 4 == 0 - if p: - if pad: - out = out[:-p] + "=" * p - else: - out = out[:-p] - return out - -def h64_decode(value, pad=False, num=False): - "decode string of bytes from hash64 format" - if value.endswith("="): - assert len(value) % 4 == 0, value - if value.endswith('=='): - p = 2 - value = value[:-2] + '..' - else: - p = 1 - value = value[:-1] + '.' - else: - #else add padding if needed - x = len(value) % 4 - if x == 0: - p = 0 - elif pad: - raise ValueError, "size must be multiple of 4" - elif x == 3: - p = 1 - value += "." - elif x == 2: - p = 2 - value += ".." - elif x == 1: - p = 3 - value += "..." - assert len(value) % 4 == 0, value - if num: - out = [] - for offset in xrange(0, len(value), 4): - out.extend(_dec64(value, offset, num=True)) - else: - out = "".join( _dec64(value, offset) for offset in xrange(0, len(value), 4)) - assert len(out) % 3 == 0 - if p: #strip out garbage chars - out = out[:-p] - return out - -def _enc64b(a, b, c, n=4): - "std hash64 bit encoding" - v = (ord(a) << 16) + (ord(b) << 8) + ord(c) - return "".join( - CHARS[(v >> (i*6)) & 0x3F] - for i in range(n) - ) - -def _enc64b1(buffer, a): - "do 64bit encode of single element of a buffer" - return _enc64b('\x00', '\x00', buffer[a], 2) - -def _enc64b2(buffer, a, b): - "do 64bit encode of 2 elements of a buffer" - return _enc64b('\x00', buffer[a], buffer[b], 3) - -def _enc64b3(buffer, a, b, c): - "do 64bit encode of 3 elements of a buffer" - return _enc64b(buffer[a], buffer[b], buffer[c], 4) - -def h64_gen_salt(size, pad=False): - "generate hash64 salt of arbitrary length" - out = ''.join( - srandom.choice(CHARS) - for idx in xrange(size) - ) - if pad and size % 4: - out += "=" * (-size % 4) - return out - class HashInfo(object): "helper used by various CryptAlgorithms to store parsed hash information" alg = None #name or alias identifying algorithm @@ -659,7 +523,7 @@ class UnixCrypt(CryptAlgorithm): if hash and keep_salt: salt = hash[:2] else: - salt = h64_gen_salt(2) + salt = H64.randstr(2) return unix_crypt(secret, salt) #default verify used @@ -685,7 +549,7 @@ class Md5Crypt(CryptAlgorithm): def _md5_crypt_raw(self, secret, salt): #init salt if not salt: - salt = h64_gen_salt(8) + salt = H64.randstr(8) assert len(salt) == 8 h = hashlib.md5() @@ -732,13 +596,13 @@ class Md5Crypt(CryptAlgorithm): hash = h.digest() out = ''.join( - _enc64b3(hash, - idx, - idx+6, + H64.encode_3_offsets(hash, idx+12 if idx < 4 else 5, + idx+6, + idx, ) for idx in xrange(5) - ) + _enc64b1(hash, 11) + ) + H64.encode_1_offset(hash, 11) return HashInfo('1', salt, out) _pat = re.compile(r""" @@ -785,333 +649,6 @@ class Md5Crypt(CryptAlgorithm): return other.chk == rec.chk #========================================================= -#ids 5,6 -- sha -#algorithm defined on this page: -# http://people.redhat.com/drepper/SHA-crypt.txt -#========================================================= -class _ShaCrypt(CryptAlgorithm): - "this is the base class used by SHA-256 & SHA-512. don't use directly." - #========================================================= - #algorithm info - #========================================================= - #hash_bits, name filled in for subclass - salt_bits = 96 - has_rounds = True - has_named_rounds = True - - #tuning the round aliases - rounds_per_second = 156000 #last tuned 2009-7-6 on a 2gz system - fast_rounds = int(rounds_per_second * .25) - medium_rounds = int(rounds_per_second * .75) - slow_rounds = int(rounds_per_second * 1.5) - - #========================================================= - #internals required from subclass - #========================================================= - _key = None #alg id (5, 6) of specific sha alg - _hash = None #callable to use for hashing - _chunk_size = None #bytes at a time to input secret - _hash_size = None #bytes in hash - _pat = None #regexp for sha variant - - @abstractmethod - def _encode(self, result): - "encode raw result into h64 style" - - #========================================================= - #core sha crypt algorithm - #========================================================= - @classmethod - def _sha_crypt_raw(self, rounds, salt, secret): - "perform sha crypt, returning just the checksum" - #setup alg-specific parameters - hash = self._hash - chunk_size = self._chunk_size - - #init salt - if salt is None: - salt = h64_gen_salt(16) - elif len(salt) > 16: - salt = salt[:16] #spec says to use up to first chars 16 only - - #init rounds - if rounds == -1: - real_rounds = 5000 - else: - if rounds < 1000: - rounds = 1000 - if rounds > 999999999: - rounds = 999999999 - real_rounds = rounds - - def extend(source, size_ref): - size = len(size_ref) - return source * int(size/chunk_size) + source[:size % chunk_size] - - #calc digest B - b = hash() - b.update(secret) - b.update(salt) - b.update(secret) - b_result = b.digest() - b_extend = extend(b_result, secret) - - #begin digest A - a = hash() - a.update(secret) - a.update(salt) - 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 - dp = hash() - dp.update(secret * len(secret)) - dp_result = extend(dp.digest(), secret) - - #calc DS - ds = hash() - for i in xrange(0, 16+ord(a_result[0])): - ds.update(salt) - ds_result = extend(ds.digest(), salt) #aka 'S' - - #calc digest C - last_result = a_result - for i in xrange(0, real_rounds): - c = hash() - if i % 2: - c.update(dp_result) - else: - c.update(last_result) - if i % 3: - c.update(ds_result) - if i % 7: - c.update(dp_result) - if i % 2: - c.update(last_result) - else: - c.update(dp_result) - last_result = c.digest() - - #encode result using 256/512 specific func - out = self._encode(last_result) - assert len(out) == self._hash_size, "wrong length: %r" % (out,) - return HashInfo(self._key, salt, out, rounds=rounds) - - @classmethod - def _sha_crypt(self, rounds, salt, secret): - rec = self._sha_crypt_raw(rounds, salt, secret) - if rec.rounds == -1: - return "$%s$%s$%s" % (rec.alg, rec.salt, rec.chk) - else: - return "$%s$rounds=%d$%s$%s" % (rec.alg, rec.rounds, rec.salt, rec.chk) - - #========================================================= - #frontend helpers - #========================================================= - @classmethod - def identify(self, hash): - "identify bcrypt hash" - if hash is None: - return False - return self._pat.match(hash) is not None - - @classmethod - def _parse(self, hash): - "parse bcrypt hash" - m = self._pat.match(hash) - if not m: - raise ValueError, "invalid sha hash/salt" - alg, rounds, salt, chk = m.group("alg", "rounds", "salt", "chk") - if rounds is None: - rounds = -1 #indicate we're using the default mode - else: - rounds = int(rounds) - assert alg == self._key - return HashInfo(alg, salt, chk, rounds=rounds, source=hash) - - @classmethod - def encrypt(self, secret, hash=None, rounds=None, keep_salt=False): - """encrypt using sha256/512-crypt. - - In addition to the normal options that :meth:`CryptAlgorithm.encrypt` takes, - this function also accepts the following: - - :param rounds: - Optionally specify the number of rounds to use. - This can be one of "fast", "medium", "slow", - or an integer in the range 1000...999999999. - - See :attr:`CryptAlgorithm.has_named_rounds` for details - on the meaning of "fast", "medium" and "slow". - """ - salt = None - if hash: - rec = self._parse(hash) - if keep_salt: - salt = rec.salt - if rounds is None: - rounds = rec.rounds - rounds = self._norm_rounds(rounds) - return self._sha_crypt(rounds, salt, secret) - - @classmethod - def _norm_rounds(self, rounds): - if isinstance(rounds, int): - return rounds - elif rounds == "fast" or rounds is None: - return self.fast_rounds - elif rounds == "slow": - return self.slow_rounds - else: - if rounds != "medium": - log.warning("unknown rounds alias %r, using 'medium'", rounds) - return self.medium_rounds - - @classmethod - def verify(self, secret, hash): - if hash is None: - return False - rec = self._parse(hash) - other = self._sha_crypt_raw(rec.rounds, rec.salt, secret) - return other.chk == rec.chk - - #========================================================= - #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' - hash_bits = 256 - - #========================================================= - #internals - #========================================================= - _hash = hashlib.sha256 - _key = '5' - _chunk_size = 32 - _hash_size = 43 - - @classmethod - def _encode(self, result): - out = '' - a, b, c = [0, 10, 20] - while a < 30: - out += _enc64b3(result, a, b, c) - a, b, c = c+1, a+1, b+1 - assert a == 30, "loop to far: %r" % (a,) - out += _enc64b2(result, 31, 30) - return out - - #========================================================= - #frontend - #========================================================= - _pat = re.compile(r""" - ^ - \$(?P<alg>5) - (\$rounds=(?P<rounds>\d+))? - \$(?P<salt>[A-Za-z0-9./]+) - (\$(?P<chk>[A-Za-z0-9./]+))? - $ - """, re.X) - - #========================================================= - #eof - #========================================================= - -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.hash 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 - """ - #========================================================= - #algorithm info - #========================================================= - - name='sha512-crypt' - hash_bits = 512 - - #========================================================= - #internals - #========================================================= - _hash = hashlib.sha512 - _key = '6' - _chunk_size = 64 - _hash_size = 86 - - @classmethod - def _encode(self, result): - out = '' - a, b, c = [0, 21, 42] - while c < 63: - out += _enc64b3(result, a, b, c) - a, b, c = b+1, c+1, a+1 - assert c == 63, "loop to far: %r" % (c,) - out += _enc64b1(result, 63) - return out - - #========================================================= - #frontend - #========================================================= - - _pat = re.compile(r""" - ^ - \$(?P<alg>6) - (\$rounds=(?P<rounds>\d+))? - \$(?P<salt>[A-Za-z0-9./]+) - (\$(?P<chk>[A-Za-z0-9./]+))? - $ - """, re.X) - - #========================================================= - #eof - #========================================================= - -#========================================================= #OpenBSD's BCrypt #========================================================= class BCrypt(CryptAlgorithm): @@ -1561,152 +1098,5 @@ def is_crypt_context(obj): )) #========================================================= -#build up the standard context objects -#========================================================= - -#default context for quick use.. recognizes all known algorithms, -# currently uses SHA-512 as default -default_context = CryptContext([ UnixCrypt, Md5Crypt, BCrypt, Sha256Crypt, Sha512Crypt ]) - -def identify(hash, resolve=False): - """Identify algorithm which generated a password hash. - - :arg hash: - The hash string to identify. - :param resolve: - If ``True``, this function will return a :class:`CryptAlgorithm` - instance which can handle the hash. - If ``False`` (the default), then only the name of the hash algorithm - will be returned. - - The following algorithms are currently recognized: - - =================== ================================================ - Name Description - ------------------- ------------------------------------------------ - ``"unix-crypt"`` the historical unix-crypt algorithm - - ``"md5-crypt"`` the md5-crypt algorithm, usually identified - by the prefix ``$1$`` in unix shadow files. - - ``"bcrypt"`` the openbsd blowfish-crypt algorithm, - usually identified by the prefixes ``$2$`` or ``$2a$`` - in unix shadow files. - - ``"sha256-crypt"`` the 256-bit version of the sha-crypt algorithm, - usually identified by the prefix ``$5$`` - in unix shadow files. - - ``"sha512-crypt"`` the 512-bit version of the sha-crypt algorithm, - usually identified by the prefix ``$6$`` - in unix shadow files. - =================== ================================================ - - :returns: - The name of the hash, or ``None`` if the hash could not be identified. - (The return may be altered by the *resolve* keyword). - - .. note:: - This is a convience wrapper for ``pwhash.default_context.identify(hash)``. - """ - return default_context.identify(hash, resolve=resolve) - -def encrypt(secret, hash=None, alg=None, **kwds): - """Encrypt secret using a password hash algorithm. - - :type secret: str - :arg secret: - String containing the secret to encrypt - - :type hash: str|None - :arg hash: - Optional previously existing hash string which - will be used to provide default value for the salt, rounds, - or other algorithm-specific options. - If not specified, algorithm-chosen defaults will be used. - - :type alg: str|None - :param alg: - Optionally specify the name of the algorithm to use. - If no algorithm is specified, an attempt is made - to guess from the hash string. If no hash string - is specified, sha512-crypt will be used. - See :func:`identify` for a list of algorithm names. - - All other keywords are passed on to the specific password algorithm - being used to encrypt the secret. - - :type keep_salt: bool - :param keep_salt: - This option is accepted by all of the builtin algorithms. - - By default, a new salt value generated each time - a secret is encrypted. However, if this keyword - is set to ``True``, and a previous hash string is provided, - the salt from that string will be used instead. - - .. note:: - This is generally only useful when verifying an existing hash - (see :func:`verify`). Other than that, this option should be - avoided, as re-using a salt will needlessly decrease security. - - :type rounds: int - :param rounds: - For the sha256-crypt and sha512-crypt algorithms, - this option lets you specify the number of rounds - of encryption to use. For the bcrypt algorithm, - this option lets you specify the log-base-2 of - the number of rounds of encryption to use. - - For all three of these algorithms, you can either - specify a positive integer, or one of the strings - "fast", "medium", "slow" to choose a preset number - of rounds corresponding to an appropriate level - of encryption. - - :returns: - The secret as encoded by the specified algorithm and options. - """ - return default_context.encrypt(secret, hash=hash, alg=alg, **kwds) - -def verify(secret, hash, alg=None): - """verify a secret against an existing hash. - - This checks if a secret matches against the one stored - inside the specified hash. By default this uses :func:`encrypt` - to re-crypt the secret, and compares it to the provided hash; - though some algorithms may implement this in a more efficient manner. - - :type secret: str - :arg secret: - A string containing the secret to check. - - :type hash: str - :param hash: - A string containing the hash to check against. - - :type alg: str|None - :param alg: - Optionally specify the name of the algorithm to use. - If no algorithm is specified, an attempt is made - to guess from the hash string. If it can't be - identified, a ValueError will be raised. - See :func:`identify` for a list of algorithm names. - - :returns: - ``True`` if the secret matches, otherwise ``False``. - """ - return default_context.verify(secret, hash, alg=alg) - -#some general os-context helpers (these may not match your os policy exactly) -linux_context = CryptContext([ UnixCrypt, Md5Crypt, Sha256Crypt, Sha512Crypt ]) -bsd_context = CryptContext([ UnixCrypt, Md5Crypt, BCrypt ]) - -#some sql db context helpers -mysql40_context = CryptContext([Mysql10Crypt]) -mysql_context = CryptContext([Mysql10Crypt, Mysql41Crypt]) -postgres_context = CryptContext([PostgresMd5Crypt]) - -#========================================================= # eof #========================================================= diff --git a/passlib/hash/sha_crypt.py b/passlib/hash/sha_crypt.py new file mode 100644 index 0000000..22bd590 --- /dev/null +++ b/passlib/hash/sha_crypt.py @@ -0,0 +1,355 @@ +"""passlib.hash.sha_crypt - implements SHA-256-Crypt & SHA-512-Crypt + +Implementation written based on specification at `<http://www.akkadia.org/drepper/SHA-crypt.txt>`_. +It should be byte-compatible with unix shadow hashes beginning with ``$5$``. +""" +#========================================================= +#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.hash.base import CryptAlgorithm, HashInfo +from passlib.util import classproperty, abstractmethod, is_seq, srandom, H64 +#pkg +#local +__all__ = [ + 'Sha256Crypt', + 'Sha512Crypt', +] + +#========================================================= +#ids 5,6 -- sha +#algorithm defined on this page: +# http://people.redhat.com/drepper/SHA-crypt.txt +#========================================================= +class _ShaCrypt(CryptAlgorithm): + "common code for used by Sha(256|512)Crypt Classes" + #========================================================= + #algorithm info + #========================================================= + #hash_bits, name filled in for subclass + salt_bits = 96 + has_rounds = True + has_named_rounds = True + + #tuning the round aliases + rounds_per_second = 156000 #last tuned 2009-7-6 on a 2gz system + fast_rounds = int(rounds_per_second * .25) + medium_rounds = int(rounds_per_second * .75) + slow_rounds = int(rounds_per_second * 1.5) + + #========================================================= + #internals required from subclass + #========================================================= + _key = None #alg id (5, 6) of specific sha alg + _hash = None #callable to use for hashing + _chunk_size = None #bytes at a time to input secret + _hash_size = None #bytes in hash + _pat = None #regexp for sha variant + + @abstractmethod + def _encode(self, result): + "encode raw result into h64 style" + + #========================================================= + #core sha crypt algorithm + #========================================================= + @classmethod + def _sha_crypt_raw(self, rounds, salt, secret): + "perform sha crypt, returning just the checksum" + #setup alg-specific parameters + hash = self._hash + chunk_size = self._chunk_size + + #init salt + if salt is None: + salt = H64.randstr(16) + elif len(salt) > 16: + salt = salt[:16] #spec says to use up to first chars 16 only + + #init rounds + if rounds == -1: + real_rounds = 5000 + else: + if rounds < 1000: + rounds = 1000 + if rounds > 999999999: + rounds = 999999999 + real_rounds = rounds + + def extend(source, size_ref): + size = len(size_ref) + return source * int(size/chunk_size) + source[:size % chunk_size] + + #calc digest B + b = hash() + b.update(secret) + b.update(salt) + b.update(secret) + b_result = b.digest() + b_extend = extend(b_result, secret) + + #begin digest A + a = hash() + a.update(secret) + a.update(salt) + 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 + dp = hash() + dp.update(secret * len(secret)) + dp_result = extend(dp.digest(), secret) + + #calc DS + ds = hash() + for i in xrange(0, 16+ord(a_result[0])): + ds.update(salt) + ds_result = extend(ds.digest(), salt) #aka 'S' + + #calc digest C + last_result = a_result + for i in xrange(0, real_rounds): + c = hash() + if i % 2: + c.update(dp_result) + else: + c.update(last_result) + if i % 3: + c.update(ds_result) + if i % 7: + c.update(dp_result) + if i % 2: + c.update(last_result) + else: + c.update(dp_result) + last_result = c.digest() + + #encode result using 256/512 specific func + out = self._encode(last_result) + assert len(out) == self._hash_size, "wrong length: %r" % (out,) + return HashInfo(self._key, salt, out, rounds=rounds) + + @classmethod + def _sha_crypt(self, rounds, salt, secret): + rec = self._sha_crypt_raw(rounds, salt, secret) + if rec.rounds == -1: + return "$%s$%s$%s" % (rec.alg, rec.salt, rec.chk) + else: + return "$%s$rounds=%d$%s$%s" % (rec.alg, rec.rounds, rec.salt, rec.chk) + + #========================================================= + #frontend helpers + #========================================================= + @classmethod + def identify(self, hash): + "identify bcrypt hash" + if hash is None: + return False + return self._pat.match(hash) is not None + + @classmethod + def _parse(self, hash): + "parse bcrypt hash" + m = self._pat.match(hash) + if not m: + raise ValueError, "invalid sha hash/salt" + alg, rounds, salt, chk = m.group("alg", "rounds", "salt", "chk") + if rounds is None: + rounds = -1 #indicate we're using the default mode + else: + rounds = int(rounds) + assert alg == self._key + return HashInfo(alg, salt, chk, rounds=rounds, source=hash) + + @classmethod + def encrypt(self, secret, hash=None, rounds=None, keep_salt=False): + """encrypt using sha256/512-crypt. + + In addition to the normal options that :meth:`CryptAlgorithm.encrypt` takes, + this function also accepts the following: + + :param rounds: + Optionally specify the number of rounds to use. + This can be one of "fast", "medium", "slow", + or an integer in the range 1000...999999999. + + See :attr:`CryptAlgorithm.has_named_rounds` for details + on the meaning of "fast", "medium" and "slow". + """ + salt = None + if hash: + rec = self._parse(hash) + if keep_salt: + salt = rec.salt + if rounds is None: + rounds = rec.rounds + rounds = self._norm_rounds(rounds) + return self._sha_crypt(rounds, salt, secret) + + @classmethod + def _norm_rounds(self, rounds): + if isinstance(rounds, int): + return rounds + elif rounds == "fast" or rounds is None: + return self.fast_rounds + elif rounds == "slow": + return self.slow_rounds + else: + if rounds != "medium": + log.warning("unknown rounds alias %r, using 'medium'", rounds) + return self.medium_rounds + + @classmethod + def verify(self, secret, hash): + if hash is None: + return False + rec = self._parse(hash) + other = self._sha_crypt_raw(rec.rounds, rec.salt, secret) + return other.chk == rec.chk + + #========================================================= + #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='sha-256-crypt' + hash_bits = 256 + + #========================================================= + #internals + #========================================================= + _hash = hashlib.sha256 + _key = '5' + _chunk_size = 32 + _hash_size = 43 + + @classmethod + def _encode(self, 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) + return out + + #========================================================= + #frontend + #========================================================= + _pat = re.compile(r""" + ^ + \$(?P<alg>5) + (\$rounds=(?P<rounds>\d+))? + \$(?P<salt>[A-Za-z0-9./]+) + (\$(?P<chk>[A-Za-z0-9./]+))? + $ + """, re.X) + + #========================================================= + #eof + #========================================================= + +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.hash 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 + """ + #========================================================= + #algorithm info + #========================================================= + name='sha-512-crypt' + hash_bits = 512 + + #========================================================= + #internals + #========================================================= + _hash = hashlib.sha512 + _key = '6' + _chunk_size = 64 + _hash_size = 86 + + @classmethod + def _encode(self, 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) + return out + + #========================================================= + #frontend + #========================================================= + + _pat = re.compile(r""" + ^ + \$(?P<alg>6) + (\$rounds=(?P<rounds>\d+))? + \$(?P<salt>[A-Za-z0-9./]+) + (\$(?P<chk>[A-Za-z0-9./]+))? + $ + """, re.X) + + #========================================================= + #eof + #========================================================= + +#========================================================= +# eof +#========================================================= diff --git a/passlib/hash/unix_crypt.py b/passlib/hash/unix_crypt.py new file mode 100644 index 0000000..697eca7 --- /dev/null +++ b/passlib/hash/unix_crypt.py @@ -0,0 +1,96 @@ +"""passlib.hash - implementation of various password hashing functions""" +#========================================================= +#imports +#========================================================= +from __future__ import with_statement +#core +import inspect +import re +import hashlib +import logging; log = logging.getLogger(__name__) +import time +import os +try: + #try stdlib module, which is only present under posix + from crypt import crypt as _crypt + + #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 or len(salt) < 2: + raise ValueError, "invalid salt" + elif salt[0] not in H64.CHARS or salt[1] not in H64.CHARS: + raise ValueError, "invalid salt" + return _crypt(key, salt) + + backend = "stdlib" +except ImportError: + #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._unix_crypt import crypt + backend = "builtin" +#site +#pkg +from passlib.hash.base import CryptAlgorithm, HashInfo +from passlib.util import classproperty, abstractmethod, is_seq, srandom, H64 +#local +__all__ = [ + 'UnixCrypt', + 'crypt', +] + +#========================================================= +#old unix crypt +#========================================================= + +class UnixCrypt(CryptAlgorithm): + """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. + """ + name = "unix-crypt" + salt_bits = 6*2 + hash_bits = 6*11 + has_rounds = False + secret_chars = 8 + + #FORMAT: 2 chars of H64-encoded salt + 11 chars of H64-encoded checksum + _pat = re.compile(r""" + ^ + (?P<salt>[./a-z0-9]{2}) + (?P<hash>[./a-z0-9]{11}) + $""", re.X|re.I) + + @classmethod + def identify(self, hash): + if hash is None: + return False + return self._pat.match(hash) is not None + + @classmethod + def encrypt(self, secret, hash=None, keep_salt=False): + if hash and keep_salt: + salt = hash[:2] + else: + salt = H64.randstr(2) + return crypt(secret, salt) + + #default verify used + +#========================================================= +# eof +#========================================================= diff --git a/passlib/tests/test_crypt_context.py b/passlib/tests/test_crypt_context.py new file mode 100644 index 0000000..6dc43e2 --- /dev/null +++ b/passlib/tests/test_crypt_context.py @@ -0,0 +1,601 @@ +"""tests for passlib.pwhash -- (c) Assurance Technologies 2003-2009""" +#========================================================= +#imports +#========================================================= +from __future__ import with_statement +#core +import hashlib +import warnings +from logging import getLogger +#site +#pkg +from passlib import hash as pwhash +from passlib.tests.utils import TestCase, enable_suite +from passlib.util import H64 +from passlib.hash.unix_crypt import UnixCrypt +from passlib.hash.sha_crypt import Sha512Crypt +from passlib.hash.base import Md5Crypt +from passlib.tests.test_hash import UnsaltedAlg, SaltedAlg, SampleAlg +#module +log = getLogger(__name__) + +#========================================================= +#CryptContext +#========================================================= + +CryptContext = pwhash.CryptContext + +class CryptContextTest(TestCase): + "test CryptContext object's behavior" + + #========================================================= + #0 constructor + #========================================================= + def test_00_constructor(self): + "test CryptContext constructor using classes" + #create crypt context + cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg]) + + #parse + a, b, c = cc + self.assertIsInstance(a, UnsaltedAlg) + self.assertIsInstance(b, SaltedAlg) + self.assertIsInstance(c, SampleAlg) + + def test_01_constructor(self): + "test CryptContext constructor using instances" + #create crypt context + a = UnsaltedAlg() + b = SaltedAlg() + c = SampleAlg() + cc = CryptContext([a,b,c]) + + #verify elements + self.assertEquals(list(cc), [a, b, c]) + + #========================================================= + #1 list getters + #========================================================= + def test_10_getitem(self): + "test CryptContext.__getitem__[idx]" + #create crypt context + cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg]) + a, b, c = cc + + #verify len + self.assertEquals(len(cc), 3) + + #verify getitem + self.assertEquals(cc[0], a) + self.assertEquals(cc[1], b) + self.assertEquals(cc[2], c) + self.assertEquals(cc[-1], c) + self.assertRaises(IndexError, cc.__getitem__, 3) + + def test_11_index(self): + "test CryptContext.index(elem)" + #create crypt context + cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg]) + a, b, c = cc + d = SampleAlg() + + self.assertEquals(cc.index(a), 0) + self.assertEquals(cc.index(b), 1) + self.assertEquals(cc.index(c), 2) + self.assertEquals(cc.index(d), -1) + + def test_12_contains(self): + "test CryptContext.__contains__(elem)" + #create crypt context + cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg]) + a, b, c = cc + d = SampleAlg() + + self.assertEquals(a in cc, True) + self.assertEquals(b in cc, True) + self.assertEquals(c in cc, True) + self.assertEquals(d in cc, False) + + #========================================================= + #2 list setters + #========================================================= + def test_20_setitem(self): + "test CryptContext.__setitem__" + cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg]) + a, b, c = cc + d = SampleAlg() + self.assertIsNot(c, d) + e = pwhash.Md5Crypt() + + #check baseline + self.assertEquals(list(cc), [a, b, c]) + + #replace 0 w/ d should raise error (SampleAlg already in list) + self.assertRaises(KeyError, cc.__setitem__, 0, d) + self.assertEquals(list(cc), [a, b, c]) + + #replace 0 w/ e + cc[0] = e + self.assertEquals(list(cc), [e, b, c]) + + #replace 2 w/ d + cc[2] = d + self.assertEquals(list(cc), [e, b, d]) + + #replace -1 w/ c + cc[-1] = c + self.assertEquals(list(cc), [e, b, c]) + + #replace -2 w/ d should raise error + self.assertRaises(KeyError, cc.__setitem__, -2, d) + self.assertEquals(list(cc), [e, b, c]) + + def test_21_append(self): + "test CryptContext.__setitem__" + cc = CryptContext([UnsaltedAlg]) + a, = cc + b = SaltedAlg() + c = SampleAlg() + d = SampleAlg() + + self.assertEquals(list(cc), [a]) + + #try append + cc.append(b) + self.assertEquals(list(cc), [a, b]) + + #and again + cc.append(c) + self.assertEquals(list(cc), [a, b, c]) + + #try append dup + self.assertRaises(KeyError, cc.append, d) + self.assertEquals(list(cc), [a, b, c]) + + def test_20_insert(self): + "test CryptContext.insert" + cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg]) + a, b, c = cc + d = SampleAlg() + self.assertIsNot(c, d) + e = pwhash.Md5Crypt() + f = pwhash.Sha512Crypt() + g = pwhash.UnixCrypt() + + #check baseline + self.assertEquals(list(cc), [a, b, c]) + + #inserting d at 0 should raise error (SampleAlg already in list) + self.assertRaises(KeyError, cc.insert, 0, d) + self.assertEquals(list(cc), [a, b, c]) + + #insert e at start + cc.insert(0, e) + self.assertEquals(list(cc), [e, a, b, c]) + + #insert f at end + cc.insert(-1, f) + self.assertEquals(list(cc), [e, a, b, f, c]) + + #insert g at end + cc.insert(5, g) + self.assertEquals(list(cc), [e, a, b, f, c, g]) + + #========================================================= + #3 list dellers + #========================================================= + def test_30_remove(self): + cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg]) + a, b, c = cc + d = SampleAlg() + self.assertIsNot(c, d) + + self.assertEquals(list(cc), [a, b, c]) + + self.assertRaises(ValueError, cc.remove, d) + self.assertEquals(list(cc), [a, b, c]) + + cc.remove(a) + self.assertEquals(list(cc), [b, c]) + + self.assertRaises(ValueError, cc.remove, a) + self.assertEquals(list(cc), [b, c]) + + def test_31_discard(self): + cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg]) + a, b, c = cc + d = SampleAlg() + self.assertIsNot(c, d) + + self.assertEquals(list(cc), [a, b, c]) + + self.assertEquals(cc.discard(d), False) + self.assertEquals(list(cc), [a, b, c]) + + self.assertEquals(cc.discard(a), True) + self.assertEquals(list(cc), [b, c]) + + self.assertEquals(cc.discard(a), False) + self.assertEquals(list(cc), [b, c]) + + #========================================================= + #4 list composition + #========================================================= + + def test_40_add(self, lsc=False): + "test CryptContext + list" + #build and join cc to list + a = UnsaltedAlg() + b = SaltedAlg() + c = SampleAlg() + cc = CryptContext([a, b, c]) + ls = [pwhash.Md5Crypt, pwhash.Sha512Crypt] + if lsc: + ls = CryptContext(ls) + cc2 = cc + ls + + #verify types + self.assertIsInstance(cc, CryptContext) + self.assertIsInstance(cc2, CryptContext) + self.assertIsInstance(ls, CryptContext if lsc else list) + + #verify elements + self.assertIsNot(cc, ls) + self.assertIsNot(cc, cc2) + self.assertIsNot(ls, cc2) + + #verify cc + a, b, c = cc + self.assertIsInstance(a, UnsaltedAlg) + self.assertIsInstance(b, SaltedAlg) + self.assertIsInstance(c, SampleAlg) + + #verify ls + d, e = ls + if lsc: + self.assertIsInstance(d, Md5Crypt) + self.assertIsInstance(e, Sha512Crypt) + else: + self.assertIs(d, Md5Crypt) + self.assertIs(e, Sha512Crypt) + + #verify cc2 + a2, b2, c2, d2, e2 = cc2 + self.assertIs(a2, a) + self.assertIs(b2, b) + self.assertIs(c2, c) + if lsc: + self.assertIs(d2, d) + self.assertIs(e2, e) + else: + self.assertIsInstance(d2, Md5Crypt) + self.assertIsInstance(e2, Sha512Crypt) + + def test_41_add(self): + "test CryptContext + CryptContext" + self.test_40_add(lsc=True) + + def test_42_iadd(self, lsc=False): + "test CryptContext += list" + #build and join cc to list + a = UnsaltedAlg() + b = SaltedAlg() + c = SampleAlg() + cc = CryptContext([a, b, c]) + ls = [Md5Crypt, Sha512Crypt] + if lsc: + ls = CryptContext(ls) + + #baseline + self.assertEquals(list(cc), [a, b, c]) + self.assertIsInstance(cc, CryptContext) + self.assertIsInstance(ls, CryptContext if lsc else list) + if lsc: + d, e = ls + self.assertIsInstance(d, Md5Crypt) + self.assertIsInstance(e, Sha512Crypt) + + #add + cc += ls + + #verify types + self.assertIsInstance(cc, CryptContext) + self.assertIsInstance(ls, CryptContext if lsc else list) + + #verify elements + self.assertIsNot(cc, ls) + + #verify cc + a2, b2, c2, d2, e2 = cc + self.assertIs(a2, a) + self.assertIs(b2, b) + self.assertIs(c2, c) + if lsc: + self.assertIs(d2, d) + self.assertIs(e2, e) + else: + self.assertIsInstance(d2, Md5Crypt) + self.assertIsInstance(e2, Sha512Crypt) + + #verify ls + d, e = ls + if lsc: + self.assertIsInstance(d, Md5Crypt) + self.assertIsInstance(e, Sha512Crypt) + else: + self.assertIs(d, Md5Crypt) + self.assertIs(e, Sha512Crypt) + + def test_43_iadd(self): + "test CryptContext += CryptContext" + self.test_42_iadd(lsc=True) + + def test_44_extend(self): + a = UnsaltedAlg() + b = SaltedAlg() + c = SampleAlg() + cc = CryptContext([a, b, c]) + ls = [Md5Crypt, Sha512Crypt] + + cc.extend(ls) + + a2, b2, c2, d2, e2 = cc + self.assertIs(a2, a) + self.assertIs(b2, b) + self.assertIs(c2, c) + self.assertIsInstance(d2, Md5Crypt) + self.assertIsInstance(e2, Sha512Crypt) + + self.assertRaises(KeyError, cc.extend, [Sha512Crypt ]) + self.assertRaises(KeyError, cc.extend, [Sha512Crypt() ]) + + #========================================================= + #5 basic crypt interface + #========================================================= + def test_50_resolve(self): + "test CryptContext.resolve()" + cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg]) + a, b, c = cc + + self.assertEquals(cc.resolve('unsalted'), a) + self.assertEquals(cc.resolve('salted'), b) + self.assertEquals(cc.resolve('sample'), c) + self.assertEquals(cc.resolve('md5-crypt'), None) + + self.assertEquals(cc.resolve(['unsalted']), a) + self.assertEquals(cc.resolve(['md5-crypt']), None) + self.assertEquals(cc.resolve(['unsalted', 'salted', 'md5-crypt']), b) + + def test_51_identify(self): + "test CryptContext.identify" + cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg]) + a, b, c = cc + + for crypt in (a, b, c): + h = crypt.encrypt("test") + self.assertEquals(cc.identify(h, resolve=True), crypt) + self.assertEquals(cc.identify(h), crypt.name) + + self.assertEquals(cc.identify('$1$232323123$1287319827', resolve=True), None) + self.assertEquals(cc.identify('$1$232323123$1287319827'), None) + + #make sure "None" is accepted + self.assertEquals(cc.identify(None), None) + + def test_52_encrypt_and_verify(self): + "test CryptContext.encrypt & verify" + cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg]) + a, b, c = cc + + #check encrypt/id/verify pass for all algs + for crypt in (a, b, c): + h = cc.encrypt("test", alg=crypt.name) + self.assertEquals(cc.identify(h, resolve=True), crypt) + self.assertEquals(cc.verify('test', h), True) + self.assertEquals(cc.verify('notest', h), False) + + #check default alg + h = cc.encrypt("test") + self.assertEquals(cc.identify(h, resolve=True), c) + + #check verify using algs + self.assertEquals(cc.verify('test', h, alg='sample'), True) + self.assertEquals(cc.verify('test', h, alg='salted'), False) + + def test_53_encrypt_salting(self): + "test CryptContext.encrypt salting options" + cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg]) + a, b, c = cc + self.assert_(c.has_salt) + + h = cc.encrypt("test") + self.assertEquals(cc.identify(h, resolve=True), c) + + h2 = cc.encrypt("test", h) + self.assertEquals(cc.identify(h2, resolve=True), c) + self.assertNotEquals(h2, h) + + h3 = cc.encrypt("test", h, keep_salt=True) + self.assertEquals(cc.identify(h3, resolve=True), c) + self.assertEquals(h3, h) + + def test_54_verify_empty(self): + "test CryptContext.verify allows hash=None" + cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg]) + self.assertEquals(cc.verify('xxx', None), False) + for crypt in cc: + self.assertEquals(cc.verify('xxx', None, alg=crypt.name), False) + +#XXX: haven't decided if this should be part of protocol +## def test_55_verify_empty_secret(self): +## "test CryptContext.verify allows secret=None" +## cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg]) +## h = cc.encrypt("test") +## self.assertEquals(cc.verify(None,h), False) + + #========================================================= + #6 crypt-enhanced list interface + #========================================================= + def test_60_getitem(self): + "test CryptContext.__getitem__[algname]" + #create crypt context + cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg]) + a, b, c = cc + + #verify getitem + self.assertEquals(cc['unsalted'], a) + self.assertEquals(cc['salted'], b) + self.assertEquals(cc['sample'], c) + self.assertRaises(KeyError, cc.__getitem__, 'md5-crypt') + + def test_61_get(self): + "test CryptContext.get(algname)" + #create crypt context + cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg]) + a, b, c = cc + + #verify getitem + self.assertEquals(cc.get('unsalted'), a) + self.assertEquals(cc.get('salted'), b) + self.assertEquals(cc.get('sample'), c) + self.assertEquals(cc.get('md5-crypt'), None) + + def test_62_index(self): + "test CryptContext.index(algname)" + #create crypt context + cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg]) + + #verify getitem + self.assertEquals(cc.index('unsalted'), 0) + self.assertEquals(cc.index('salted'), 1) + self.assertEquals(cc.index('sample'), 2) + self.assertEquals(cc.index('md5-crypt'), -1) + + def test_63_contains(self): + "test CryptContext.__contains__(algname)" + #create crypt context + cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg]) + self.assertEquals('salted' in cc, True) + self.assertEquals('unsalted' in cc, True) + self.assertEquals('sample' in cc, True) + self.assertEquals('md5-crypt' in cc, False) + + def test_64_keys(self): + "test CryptContext.keys()" + cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg]) + self.assertEquals(cc.keys(), ['unsalted', 'salted', 'sample']) + + def test_65_remove(self): + cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg]) + a, b, c = cc + + self.assertEquals(list(cc), [a, b, c]) + + self.assertRaises(KeyError, cc.remove, 'md5-crypt') + self.assertEquals(list(cc), [a, b, c]) + + cc.remove('unsalted') + self.assertEquals(list(cc), [b, c]) + + self.assertRaises(KeyError, cc.remove, 'unsalted') + self.assertEquals(list(cc), [b, c]) + + def test_66_discard(self): + cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg]) + a, b, c = cc + + self.assertEquals(list(cc), [a, b, c]) + + self.assertEquals(cc.discard('md5-crypt'), False) + self.assertEquals(list(cc), [a, b, c]) + + self.assertEquals(cc.discard('unsalted'), True) + self.assertEquals(list(cc), [b, c]) + + self.assertEquals(cc.discard('unsalted'), False) + self.assertEquals(list(cc), [b, c]) + #========================================================= + #eoc + #========================================================= + +#========================================================= +#quick access functions +#========================================================= + +#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_unix_crypt import UnixCryptTest +from passlib.tests.test_hash import Md5CryptTest +try: + from passlib.tests.test_hash import BCryptTest +except ImportError: + BCryptTest = None + +class QuickAccessTest(TestCase): + "test quick access functions" + + crypt_cases = [ UnixCryptTest, Md5CryptTest, Sha256CryptTest] + if BCryptTest: + crypt_cases.append(BCryptTest) + crypt_cases.extend([ Sha512CryptTest ]) + + def test_00_identify(self): + "test pwhash.identify()" + identify = pwhash.identify + for cc in self.crypt_cases: + name = cc.alg.name + for _, hash in cc.positive_knowns: + self.assertEqual(identify(hash), name) + for _, hash in cc.negative_knowns: + self.assertEqual(identify(hash), name) + for hash in cc.negative_identify: + self.assertNotEqual(identify(hash), name) + for hash in cc.invalid_identify: + self.assertEqual(identify(hash), None) + + def test_01_verify(self): + "test pwhash.verify()" + verify = pwhash.verify + for cc in self.crypt_cases: + name = cc.alg.name + for secret, hash in cc.positive_knowns[:3]: + self.assert_(verify(secret, hash)) + self.assert_(verify(secret, hash, alg=name)) + for secret, hash in cc.negative_knowns[:3]: + self.assert_(not verify(secret, hash)) + self.assert_(not verify(secret, hash, alg=name)) + for hash in cc.invalid_identify[:3]: + #context should raise ValueError because can't be identified + self.assertRaises(ValueError, verify, secret, hash) + + def test_02_encrypt(self): + "test pwhash.encrypt()" + identify = pwhash.identify + verify = pwhash.verify + encrypt = pwhash.encrypt + for cc in self.crypt_cases: + alg = cc.alg.name + s = 'test' + h = encrypt(s, alg=alg) + self.assertEqual(identify(h), alg) + self.assertEqual(verify(s, h), True) + h2 = encrypt(s, h) + self.assertEqual(identify(h2), alg) + self.assertEqual(verify(s, h2, alg=alg), True) + + def test_04_default_context(self): + "test pwhash.default_context contents" + dc = pwhash.default_context + for case in self.crypt_cases: + self.assert_(case.alg.name in dc) + + last = 'sha-512-crypt' + self.assertEqual(dc.keys()[-1], last) + h = dc.encrypt("test") + self.assertEqual(dc.identify(h), last) + self.assertEqual(dc.verify('test', h, alg=last), True) + +#========================================================= +#EOF +#========================================================= diff --git a/passlib/tests/test_hash.py b/passlib/tests/test_hash.py index b17897b..f7b05bb 100644 --- a/passlib/tests/test_hash.py +++ b/passlib/tests/test_hash.py @@ -11,6 +11,7 @@ from logging import getLogger #pkg from passlib import hash as pwhash from passlib.tests.utils import TestCase, enable_suite +from passlib.util import H64 #module log = getLogger(__name__) @@ -60,7 +61,7 @@ class SaltedAlg(pwhash.CryptAlgorithm): @classmethod def encrypt(self, secret, salt=None, keep_salt=False): ## warn("keep_salt not supported by this algorithm") - real_salt = pwhash.h64_gen_salt(2) + real_salt = H64.randstr(2) return self._raw(secret, real_salt) @classmethod @@ -86,7 +87,7 @@ class SampleAlg(pwhash.CryptAlgorithm): if salt and keep_salt: real_salt = salt[4:6] else: - real_salt = pwhash.h64_gen_salt(2) + real_salt = H64.randstr(2) return "@sam%s%s" % (real_salt, hashlib.sha1(real_salt+secret).hexdigest()) #========================================================= @@ -98,11 +99,9 @@ SECRETS = [ '', ' ', 'test', - 'testa', + 'testtest', 'test test', 'test bcdef', - 'testq' - 'testtest', 'Compl3X AlphaNu3meric', '4lpHa N|_|M3r1K W/ Cur51|\\|g: #$%(*)(*%#', 'Really Long Password (tm), which is all the rage nowadays with the cool kids' @@ -120,7 +119,7 @@ class _CryptTestCase(TestCase): negative_identify = () # list of hashses that shouldn't identify as this one invalid_identify = () # list of this alg's hashes w/ typo - def message_prefix(self): + def case_prefix(self): return self.alg.name secrets = SECRETS #list of default secrets to check @@ -380,60 +379,6 @@ class PostgresMd5CryptTest(_CryptTestCase): return self.alg().verify(secret, hash, user=user) #========================================================= -#UnixCrypt -#========================================================= - -class UnixCryptTest(_CryptTestCase): - "test UnixCrypt algorithm" - alg = pwhash.UnixCrypt - positive_knowns = ( - #secret, example hash which matches secret - ('', 'OgAwTx2l6NADI'), - (' ', '/Hk.VPuwQTXbc'), - ('test', 'N1tQbOFcM5fpg'), - ('Compl3X AlphaNu3meric', 'um.Wguz3eVCx2'), - ('4lpHa N|_|M3r1K W/ Cur5Es: #$%(*)(*%#', 'sNYqfOyauIyic'), - ('AlOtBsOl', 'cEpWz5IUCShqM'), - ) - invalid_identify = ( - #bad char in otherwise correctly formatted hash - '!gAwTx2l6NADI', - ) - negative_identify = ( - #hashes using other algs, which shouldn't match this algorithm - '$6$rounds=123456$asaltof16chars..$BtCwjqMJGx5hrJhZywWvt0RLE8uZ4oPwc', - '$1$dOHYPKoP$tnxS1T8Q6VVn3kpV8cN6o.' - ) - -class UnixCryptBackendTest(TestCase): - "test builtin unix crypt backend" - unix_crypt = pwhash.unix_crypt - - positive_knowns = UnixCryptTest.positive_knowns - - def test_knowns(self): - "test lowlevel unix_crypt function" - unix_crypt = self.unix_crypt - for secret, result in self.positive_knowns: - #make sure crypt verifies using salt - out = unix_crypt(secret, result[:2]) - self.assertEqual(out, result) - #make sure crypt verifies using partial hash - out = unix_crypt(secret, result[:6]) - self.assertEqual(out, result) - #make sure crypt verifies using whole hash - out = unix_crypt(secret, result) - self.assertEqual(out, result) - - #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 - -#========================================================= #Md5Crypt #========================================================= class Md5CryptTest(_CryptTestCase): @@ -457,95 +402,6 @@ class Md5CryptTest(_CryptTestCase): ) #========================================================= -#test raw sha-crypt functions -#========================================================= -class Sha256CryptTest(_CryptTestCase): - alg = pwhash.Sha256Crypt - positive_knowns = ( - ('', '$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'), - ) - invalid_identify = ( - #bad char in otherwise correct hash - '$5$rounds=10428$uy/jIAhCetNCTtb0$YWvUOXbkqlqhyoPMpN8BMe!ZGsGx2aBvxTvDFI613c3' - ) - negative_identify = ( - #other hashes - '!gAwTx2l6NADI', - '$6$rounds=123456$asaltof16chars..$BtCwjqMJGx5hrJhZywWvt0RLE8uZ4oPwc', - '$1$dOHYPKoP$tnxS1T8Q6VVn3kpV8cN6ox', - ) - -class Sha512CryptTest(_CryptTestCase): - alg = pwhash.Sha512Crypt - positive_knowns = ( - ('', '$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'), - ) - negative_identify = ( - #other hashes - '!gAwTx2l6NADI', - '$5$rounds=10428$uy/jIAhCetNCTtb0$YWvUOXbkqlqhyoPMpN8BMe.ZGsGx2aBvxTvDFI613c3', - '$1$dOHYPKoP$tnxS1T8Q6VVn3kpV8cN6ox', - ) - invalid_identify = ( - #bad char in otherwise correct hash - '$6$rounds=11021$KsvQipYPWpr9!wWP$v7xjI4X6vyVptJjB1Y02vZC5SaSijBkGmq1uJhPr3cvqvvkd42Xvo48yLVPFt8dvhCsnlUgpX.//Cxn91H4qy1', - ) - -class Sha512BackendTest(TestCase): - "test sha512-crypt backend against specification unittest" - cases512 = [ - #salt-hash, secret, result -- taken from alg definition page - ("$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): - crypt = pwhash.Sha512Crypt() - for hash, secret, result in self.cases512: - rec = crypt._parse(hash) - self.assertEqual(rec.alg, '6') - out = crypt.encrypt(secret, hash, keep_salt=True) - rec2 = crypt._parse(hash) - self.assertEqual(rec2.salt, rec.salt, "hash=%r secret=%r" % (hash, secret)) - self.assertEqual(rec2.chk, rec.chk, "hash=%r secret=%r" % (hash, secret)) - self.assertEqual(out, result, "hash=%r secret=%r" % (hash, secret)) - -#========================================================= #BCrypt #========================================================= if enable_suite("bcrypt"): @@ -609,571 +465,5 @@ class UtilsTest(TestCase): self.assertEqual(SaltedAlg().has_salt, True) #========================================================= -#CryptContext -#========================================================= - -CryptContext = pwhash.CryptContext - -class CryptContextTest(TestCase): - "test CryptContext object's behavior" - - #========================================================= - #0 constructor - #========================================================= - def test_00_constructor(self): - "test CryptContext constructor using classes" - #create crypt context - cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg]) - - #parse - a, b, c = cc - self.assertIsInstance(a, UnsaltedAlg) - self.assertIsInstance(b, SaltedAlg) - self.assertIsInstance(c, SampleAlg) - - def test_01_constructor(self): - "test CryptContext constructor using instances" - #create crypt context - a = UnsaltedAlg() - b = SaltedAlg() - c = SampleAlg() - cc = CryptContext([a,b,c]) - - #verify elements - self.assertEquals(list(cc), [a, b, c]) - - #========================================================= - #1 list getters - #========================================================= - def test_10_getitem(self): - "test CryptContext.__getitem__[idx]" - #create crypt context - cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg]) - a, b, c = cc - - #verify len - self.assertEquals(len(cc), 3) - - #verify getitem - self.assertEquals(cc[0], a) - self.assertEquals(cc[1], b) - self.assertEquals(cc[2], c) - self.assertEquals(cc[-1], c) - self.assertRaises(IndexError, cc.__getitem__, 3) - - def test_11_index(self): - "test CryptContext.index(elem)" - #create crypt context - cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg]) - a, b, c = cc - d = SampleAlg() - - self.assertEquals(cc.index(a), 0) - self.assertEquals(cc.index(b), 1) - self.assertEquals(cc.index(c), 2) - self.assertEquals(cc.index(d), -1) - - def test_12_contains(self): - "test CryptContext.__contains__(elem)" - #create crypt context - cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg]) - a, b, c = cc - d = SampleAlg() - - self.assertEquals(a in cc, True) - self.assertEquals(b in cc, True) - self.assertEquals(c in cc, True) - self.assertEquals(d in cc, False) - - #========================================================= - #2 list setters - #========================================================= - def test_20_setitem(self): - "test CryptContext.__setitem__" - cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg]) - a, b, c = cc - d = SampleAlg() - self.assertIsNot(c, d) - e = pwhash.Md5Crypt() - - #check baseline - self.assertEquals(list(cc), [a, b, c]) - - #replace 0 w/ d should raise error (SampleAlg already in list) - self.assertRaises(KeyError, cc.__setitem__, 0, d) - self.assertEquals(list(cc), [a, b, c]) - - #replace 0 w/ e - cc[0] = e - self.assertEquals(list(cc), [e, b, c]) - - #replace 2 w/ d - cc[2] = d - self.assertEquals(list(cc), [e, b, d]) - - #replace -1 w/ c - cc[-1] = c - self.assertEquals(list(cc), [e, b, c]) - - #replace -2 w/ d should raise error - self.assertRaises(KeyError, cc.__setitem__, -2, d) - self.assertEquals(list(cc), [e, b, c]) - - def test_21_append(self): - "test CryptContext.__setitem__" - cc = CryptContext([UnsaltedAlg]) - a, = cc - b = SaltedAlg() - c = SampleAlg() - d = SampleAlg() - - self.assertEquals(list(cc), [a]) - - #try append - cc.append(b) - self.assertEquals(list(cc), [a, b]) - - #and again - cc.append(c) - self.assertEquals(list(cc), [a, b, c]) - - #try append dup - self.assertRaises(KeyError, cc.append, d) - self.assertEquals(list(cc), [a, b, c]) - - def test_20_insert(self): - "test CryptContext.insert" - cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg]) - a, b, c = cc - d = SampleAlg() - self.assertIsNot(c, d) - e = pwhash.Md5Crypt() - f = pwhash.Sha512Crypt() - g = pwhash.UnixCrypt() - - #check baseline - self.assertEquals(list(cc), [a, b, c]) - - #inserting d at 0 should raise error (SampleAlg already in list) - self.assertRaises(KeyError, cc.insert, 0, d) - self.assertEquals(list(cc), [a, b, c]) - - #insert e at start - cc.insert(0, e) - self.assertEquals(list(cc), [e, a, b, c]) - - #insert f at end - cc.insert(-1, f) - self.assertEquals(list(cc), [e, a, b, f, c]) - - #insert g at end - cc.insert(5, g) - self.assertEquals(list(cc), [e, a, b, f, c, g]) - - #========================================================= - #3 list dellers - #========================================================= - def test_30_remove(self): - cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg]) - a, b, c = cc - d = SampleAlg() - self.assertIsNot(c, d) - - self.assertEquals(list(cc), [a, b, c]) - - self.assertRaises(ValueError, cc.remove, d) - self.assertEquals(list(cc), [a, b, c]) - - cc.remove(a) - self.assertEquals(list(cc), [b, c]) - - self.assertRaises(ValueError, cc.remove, a) - self.assertEquals(list(cc), [b, c]) - - def test_31_discard(self): - cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg]) - a, b, c = cc - d = SampleAlg() - self.assertIsNot(c, d) - - self.assertEquals(list(cc), [a, b, c]) - - self.assertEquals(cc.discard(d), False) - self.assertEquals(list(cc), [a, b, c]) - - self.assertEquals(cc.discard(a), True) - self.assertEquals(list(cc), [b, c]) - - self.assertEquals(cc.discard(a), False) - self.assertEquals(list(cc), [b, c]) - - #========================================================= - #4 list composition - #========================================================= - - def test_40_add(self, lsc=False): - "test CryptContext + list" - #build and join cc to list - a = UnsaltedAlg() - b = SaltedAlg() - c = SampleAlg() - cc = CryptContext([a, b, c]) - ls = [pwhash.Md5Crypt, pwhash.Sha512Crypt] - if lsc: - ls = CryptContext(ls) - cc2 = cc + ls - - #verify types - self.assertIsInstance(cc, CryptContext) - self.assertIsInstance(cc2, CryptContext) - self.assertIsInstance(ls, CryptContext if lsc else list) - - #verify elements - self.assertIsNot(cc, ls) - self.assertIsNot(cc, cc2) - self.assertIsNot(ls, cc2) - - #verify cc - a, b, c = cc - self.assertIsInstance(a, UnsaltedAlg) - self.assertIsInstance(b, SaltedAlg) - self.assertIsInstance(c, SampleAlg) - - #verify ls - d, e = ls - if lsc: - self.assertIsInstance(d, pwhash.Md5Crypt) - self.assertIsInstance(e, pwhash.Sha512Crypt) - else: - self.assertIs(d, pwhash.Md5Crypt) - self.assertIs(e, pwhash.Sha512Crypt) - - #verify cc2 - a2, b2, c2, d2, e2 = cc2 - self.assertIs(a2, a) - self.assertIs(b2, b) - self.assertIs(c2, c) - if lsc: - self.assertIs(d2, d) - self.assertIs(e2, e) - else: - self.assertIsInstance(d2, pwhash.Md5Crypt) - self.assertIsInstance(e2, pwhash.Sha512Crypt) - - def test_41_add(self): - "test CryptContext + CryptContext" - self.test_40_add(lsc=True) - - def test_42_iadd(self, lsc=False): - "test CryptContext += list" - #build and join cc to list - a = UnsaltedAlg() - b = SaltedAlg() - c = SampleAlg() - cc = CryptContext([a, b, c]) - ls = [pwhash.Md5Crypt, pwhash.Sha512Crypt] - if lsc: - ls = CryptContext(ls) - - #baseline - self.assertEquals(list(cc), [a, b, c]) - self.assertIsInstance(cc, CryptContext) - self.assertIsInstance(ls, CryptContext if lsc else list) - if lsc: - d, e = ls - self.assertIsInstance(d, pwhash.Md5Crypt) - self.assertIsInstance(e, pwhash.Sha512Crypt) - - #add - cc += ls - - #verify types - self.assertIsInstance(cc, CryptContext) - self.assertIsInstance(ls, CryptContext if lsc else list) - - #verify elements - self.assertIsNot(cc, ls) - - #verify cc - a2, b2, c2, d2, e2 = cc - self.assertIs(a2, a) - self.assertIs(b2, b) - self.assertIs(c2, c) - if lsc: - self.assertIs(d2, d) - self.assertIs(e2, e) - else: - self.assertIsInstance(d2, pwhash.Md5Crypt) - self.assertIsInstance(e2, pwhash.Sha512Crypt) - - #verify ls - d, e = ls - if lsc: - self.assertIsInstance(d, pwhash.Md5Crypt) - self.assertIsInstance(e, pwhash.Sha512Crypt) - else: - self.assertIs(d, pwhash.Md5Crypt) - self.assertIs(e, pwhash.Sha512Crypt) - - def test_43_iadd(self): - "test CryptContext += CryptContext" - self.test_42_iadd(lsc=True) - - def test_44_extend(self): - a = UnsaltedAlg() - b = SaltedAlg() - c = SampleAlg() - cc = CryptContext([a, b, c]) - ls = [pwhash.Md5Crypt, pwhash.Sha512Crypt] - - cc.extend(ls) - - a2, b2, c2, d2, e2 = cc - self.assertIs(a2, a) - self.assertIs(b2, b) - self.assertIs(c2, c) - self.assertIsInstance(d2, pwhash.Md5Crypt) - self.assertIsInstance(e2, pwhash.Sha512Crypt) - - self.assertRaises(KeyError, cc.extend, [pwhash.Sha512Crypt ]) - self.assertRaises(KeyError, cc.extend, [pwhash.Sha512Crypt() ]) - - #========================================================= - #5 basic crypt interface - #========================================================= - def test_50_resolve(self): - "test CryptContext.resolve()" - cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg]) - a, b, c = cc - - self.assertEquals(cc.resolve('unsalted'), a) - self.assertEquals(cc.resolve('salted'), b) - self.assertEquals(cc.resolve('sample'), c) - self.assertEquals(cc.resolve('md5-crypt'), None) - - self.assertEquals(cc.resolve(['unsalted']), a) - self.assertEquals(cc.resolve(['md5-crypt']), None) - self.assertEquals(cc.resolve(['unsalted', 'salted', 'md5-crypt']), b) - - def test_51_identify(self): - "test CryptContext.identify" - cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg]) - a, b, c = cc - - for crypt in (a, b, c): - h = crypt.encrypt("test") - self.assertEquals(cc.identify(h, resolve=True), crypt) - self.assertEquals(cc.identify(h), crypt.name) - - self.assertEquals(cc.identify('$1$232323123$1287319827', resolve=True), None) - self.assertEquals(cc.identify('$1$232323123$1287319827'), None) - - #make sure "None" is accepted - self.assertEquals(cc.identify(None), None) - - def test_52_encrypt_and_verify(self): - "test CryptContext.encrypt & verify" - cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg]) - a, b, c = cc - - #check encrypt/id/verify pass for all algs - for crypt in (a, b, c): - h = cc.encrypt("test", alg=crypt.name) - self.assertEquals(cc.identify(h, resolve=True), crypt) - self.assertEquals(cc.verify('test', h), True) - self.assertEquals(cc.verify('notest', h), False) - - #check default alg - h = cc.encrypt("test") - self.assertEquals(cc.identify(h, resolve=True), c) - - #check verify using algs - self.assertEquals(cc.verify('test', h, alg='sample'), True) - self.assertEquals(cc.verify('test', h, alg='salted'), False) - - def test_53_encrypt_salting(self): - "test CryptContext.encrypt salting options" - cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg]) - a, b, c = cc - self.assert_(c.has_salt) - - h = cc.encrypt("test") - self.assertEquals(cc.identify(h, resolve=True), c) - - h2 = cc.encrypt("test", h) - self.assertEquals(cc.identify(h2, resolve=True), c) - self.assertNotEquals(h2, h) - - h3 = cc.encrypt("test", h, keep_salt=True) - self.assertEquals(cc.identify(h3, resolve=True), c) - self.assertEquals(h3, h) - - def test_54_verify_empty(self): - "test CryptContext.verify allows hash=None" - cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg]) - self.assertEquals(cc.verify('xxx', None), False) - for crypt in cc: - self.assertEquals(cc.verify('xxx', None, alg=crypt.name), False) - -#XXX: haven't decided if this should be part of protocol -## def test_55_verify_empty_secret(self): -## "test CryptContext.verify allows secret=None" -## cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg]) -## h = cc.encrypt("test") -## self.assertEquals(cc.verify(None,h), False) - - #========================================================= - #6 crypt-enhanced list interface - #========================================================= - def test_60_getitem(self): - "test CryptContext.__getitem__[algname]" - #create crypt context - cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg]) - a, b, c = cc - - #verify getitem - self.assertEquals(cc['unsalted'], a) - self.assertEquals(cc['salted'], b) - self.assertEquals(cc['sample'], c) - self.assertRaises(KeyError, cc.__getitem__, 'md5-crypt') - - def test_61_get(self): - "test CryptContext.get(algname)" - #create crypt context - cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg]) - a, b, c = cc - - #verify getitem - self.assertEquals(cc.get('unsalted'), a) - self.assertEquals(cc.get('salted'), b) - self.assertEquals(cc.get('sample'), c) - self.assertEquals(cc.get('md5-crypt'), None) - - def test_62_index(self): - "test CryptContext.index(algname)" - #create crypt context - cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg]) - - #verify getitem - self.assertEquals(cc.index('unsalted'), 0) - self.assertEquals(cc.index('salted'), 1) - self.assertEquals(cc.index('sample'), 2) - self.assertEquals(cc.index('md5-crypt'), -1) - - def test_63_contains(self): - "test CryptContext.__contains__(algname)" - #create crypt context - cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg]) - self.assertEquals('salted' in cc, True) - self.assertEquals('unsalted' in cc, True) - self.assertEquals('sample' in cc, True) - self.assertEquals('md5-crypt' in cc, False) - - def test_64_keys(self): - "test CryptContext.keys()" - cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg]) - self.assertEquals(cc.keys(), ['unsalted', 'salted', 'sample']) - - def test_65_remove(self): - cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg]) - a, b, c = cc - - self.assertEquals(list(cc), [a, b, c]) - - self.assertRaises(KeyError, cc.remove, 'md5-crypt') - self.assertEquals(list(cc), [a, b, c]) - - cc.remove('unsalted') - self.assertEquals(list(cc), [b, c]) - - self.assertRaises(KeyError, cc.remove, 'unsalted') - self.assertEquals(list(cc), [b, c]) - - def test_66_discard(self): - cc = CryptContext([UnsaltedAlg, SaltedAlg, SampleAlg]) - a, b, c = cc - - self.assertEquals(list(cc), [a, b, c]) - - self.assertEquals(cc.discard('md5-crypt'), False) - self.assertEquals(list(cc), [a, b, c]) - - self.assertEquals(cc.discard('unsalted'), True) - self.assertEquals(list(cc), [b, c]) - - self.assertEquals(cc.discard('unsalted'), False) - self.assertEquals(list(cc), [b, c]) - #========================================================= - #eoc - #========================================================= - -#========================================================= -#quick access functions -#========================================================= -class QuickAccessTest(TestCase): - "test quick access functions" - - crypt_cases = [ UnixCryptTest, Md5CryptTest, Sha256CryptTest] - if BCryptTest: - crypt_cases.append(BCryptTest) - crypt_cases.extend([ Sha512CryptTest ]) - - def test_00_identify(self): - "test pwhash.identify()" - identify = pwhash.identify - for cc in self.crypt_cases: - name = cc.alg.name - for _, hash in cc.positive_knowns: - self.assertEqual(identify(hash), name) - for _, hash in cc.negative_knowns: - self.assertEqual(identify(hash), name) - for hash in cc.negative_identify: - self.assertNotEqual(identify(hash), name) - for hash in cc.invalid_identify: - self.assertEqual(identify(hash), None) - - def test_01_verify(self): - "test pwhash.verify()" - verify = pwhash.verify - for cc in self.crypt_cases: - name = cc.alg.name - for secret, hash in cc.positive_knowns[:3]: - self.assert_(verify(secret, hash)) - self.assert_(verify(secret, hash, alg=name)) - for secret, hash in cc.negative_knowns[:3]: - self.assert_(not verify(secret, hash)) - self.assert_(not verify(secret, hash, alg=name)) - for hash in cc.invalid_identify[:3]: - #context should raise ValueError because can't be identified - self.assertRaises(ValueError, verify, secret, hash) - - def test_02_encrypt(self): - "test pwhash.encrypt()" - identify = pwhash.identify - verify = pwhash.verify - encrypt = pwhash.encrypt - for cc in self.crypt_cases: - alg = cc.alg.name - s = 'test' - h = encrypt(s, alg=alg) - self.assertEqual(identify(h), alg) - self.assertEqual(verify(s, h), True) - h2 = encrypt(s, h) - self.assertEqual(identify(h2), alg) - self.assertEqual(verify(s, h2, alg=alg), True) - - def test_04_default_context(self): - "test pwhash.default_context contents" - dc = pwhash.default_context - for case in self.crypt_cases: - self.assert_(case.alg.name in dc) - - last = 'sha512-crypt' - self.assertEqual(dc.keys()[-1], last) - h = dc.encrypt("test") - self.assertEqual(dc.identify(h), last) - self.assertEqual(dc.verify('test', h, alg=last), True) - -#========================================================= #EOF #========================================================= diff --git a/passlib/tests/test_sha_crypt.py b/passlib/tests/test_sha_crypt.py new file mode 100644 index 0000000..1e896a6 --- /dev/null +++ b/passlib/tests/test_sha_crypt.py @@ -0,0 +1,130 @@ +"""tests for passlib.pwhash -- (c) Assurance Technologies 2003-2009""" +#========================================================= +#imports +#========================================================= +from __future__ import with_statement +#core +import hashlib +import warnings +from logging import getLogger +#site +#pkg +from passlib import hash as pwhash +from passlib.tests.utils import TestCase, enable_suite +from passlib.tests.test_hash import _CryptTestCase as CryptTestCase +from passlib.util import H64 +import passlib.hash.sha_crypt as mod +#module +log = getLogger(__name__) + +#========================================================= +#test raw sha-crypt implementation +#========================================================= +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): + crypt = mod.Sha512Crypt + for hash, secret, result in self.cases512: + + #parse hash + rec = crypt._parse(hash) + + #ensure identifier read correctly + self.assertEqual(rec.alg, '6') + + #encrypt secret, preserving rounds & salt + out = crypt.encrypt(secret, hash, keep_salt=True) + + #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 = crypt._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(CryptTestCase): + alg = mod.Sha256Crypt + positive_knowns = ( + ('', '$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'), + ) + invalid_identify = ( + #bad char in otherwise correct hash + '$5$rounds=10428$uy/jIAhCetNCTtb0$YWvUOXbkqlqhyoPMpN8BMe!ZGsGx2aBvxTvDFI613c3' + ) + negative_identify = ( + #other hashes + '!gAwTx2l6NADI', + '$6$rounds=123456$asaltof16chars..$BtCwjqMJGx5hrJhZywWvt0RLE8uZ4oPwc', + '$1$dOHYPKoP$tnxS1T8Q6VVn3kpV8cN6ox', + ) + +class Sha512CryptTest(CryptTestCase): + alg = mod.Sha512Crypt + positive_knowns = ( + ('', '$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'), + ) + negative_identify = ( + #other hashes + '!gAwTx2l6NADI', + '$5$rounds=10428$uy/jIAhCetNCTtb0$YWvUOXbkqlqhyoPMpN8BMe.ZGsGx2aBvxTvDFI613c3', + '$1$dOHYPKoP$tnxS1T8Q6VVn3kpV8cN6ox', + ) + invalid_identify = ( + #bad char in otherwise correct hash + '$6$rounds=11021$KsvQipYPWpr9!wWP$v7xjI4X6vyVptJjB1Y02vZC5SaSijBkGmq1uJhPr3cvqvvkd42Xvo48yLVPFt8dvhCsnlUgpX.//Cxn91H4qy1', + ) + +#========================================================= +#EOF +#========================================================= diff --git a/passlib/tests/test_unix_crypt.py b/passlib/tests/test_unix_crypt.py new file mode 100644 index 0000000..a8ad7e3 --- /dev/null +++ b/passlib/tests/test_unix_crypt.py @@ -0,0 +1,121 @@ +"""tests for passlib.pwhash -- (c) Assurance Technologies 2003-2009""" +#========================================================= +#imports +#========================================================= +from __future__ import with_statement +#core +import hashlib +import warnings +from logging import getLogger +#site +#pkg +from passlib.tests.utils import TestCase, enable_suite +from passlib.util import H64 +from passlib._unix_crypt import crypt as builtin_crypt +import passlib.hash.unix_crypt as mod +from passlib.tests.test_hash import _CryptTestCase as CryptTestCase +#module +log = getLogger(__name__) + +#========================================================= +#test frontend class +#========================================================= +class UnixCryptTest(CryptTestCase): + "test UnixCrypt algorithm" + alg = mod.UnixCrypt + positive_knowns = ( + #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'), + ) + invalid_identify = ( + #bad char in otherwise correctly formatted hash + '!gAwTx2l6NADI', + ) + negative_identify = ( + #hashes using other algs, which shouldn't match this algorithm + '$6$rounds=123456$asaltof16chars..$BtCwjqMJGx5hrJhZywWvt0RLE8uZ4oPwc', + '$1$dOHYPKoP$tnxS1T8Q6VVn3kpV8cN6o.' + ) + +#========================================================= +#test activate backend (stored in mod._crypt) +#========================================================= +class UnixCryptBackendTest(TestCase): + "test builtin unix crypt backend" + case_prefix = "builtin crypt() backend" + + def get_crypt(self): + return builtin_crypt + + positive_knowns = UnixCryptTest.positive_knowns + + def test_knowns(self): + "test known crypt results" + crypt = self.get_crypt() + for secret, result in self.positive_knowns: + + #make sure crypt verifies preserving just salt + out = crypt(secret, result[:2]) + self.assertEqual(out, result) + + #make sure crypt verifies preseving salt + fragment of known hash + out = crypt(secret, result[:6]) + self.assertEqual(out, result) + + #make sure crypt verifies using whole known hash + out = crypt(secret, result) + self.assertEqual(out, result) + + #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.hash.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": + #NOTE: this will generally be the stdlib implementation, + #which of course is correct, so doing this more to detect deviations in builtin implementation + class ActiveUnixCryptBackendTest(UnixCryptBackendTest): + "test active unix crypt backend" + case_prefix = mod.backend + " crypt() backend" + + def get_crypt(self): + return mod.crypt + +#========================================================= +#EOF +#========================================================= diff --git a/passlib/tests/test_util.py b/passlib/tests/test_util.py index d8b872b..56ded3d 100644 --- a/passlib/tests/test_util.py +++ b/passlib/tests/test_util.py @@ -4,6 +4,7 @@ #========================================================= #core import sys +import random #site #pkg #module @@ -87,5 +88,53 @@ class BytesTest(TestCase): self.assertEqual(util.bytes_to_list('\x00\x00\x01', order="native"), [0, 0, 1]) #========================================================= +#hash64 +#========================================================= +class Test_H64(TestCase): + "test H64 codec functions" + case_prefix = "H64 codec" + + def test_encode_1_offset(self): + self.assertFunctionResults(util.H64.encode_1_offset,[ + ("z1", "\xff", 0), + ("..", "\x00", 0), + ]) + + def test_encode_2_offsets(self): + self.assertFunctionResults(util.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(util.H64.encode_3_offsets,[ + #move through each byte, keep offsets + ("..kz", "\x00\x00\xff", 0, 1, 2), + (".wD.", "\x00\xff\x00", 0, 1, 2), + ("z1..", "\xff\x00\x00", 0, 1, 2), + + #move through each offset, keep bytes + (".wD.", "\x00\x00\xff", 0, 2, 1), + ("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.randstr(*a, **k) + self.assertFunctionResults(wrapper,[ + ("", 0), + ("x", 1), + ("xQ", 2), + ("xQ.uwZe3lD/mKbb7", 16), + ("xQ.uwZe3lD/mKbb795.Tx2WRa3ZFXdSK", 32), + ]) + + +#========================================================= #EOF #========================================================= diff --git a/passlib/tests/utils.py b/passlib/tests/utils.py index 5ca215e..1f56fc5 100644 --- a/passlib/tests/utils.py +++ b/passlib/tests/utils.py @@ -19,6 +19,15 @@ __all__ = [ #========================================================= class Params(object): "helper to represent params for function call" + + @classmethod + def norm(cls, value): + if isinstance(value, cls): + return value + if isinstance(value, (list,tuple)): + return cls(*value) + return cls(**value) + def __init__(self, *args, **kwds): self.args = args self.kwds = kwds @@ -43,17 +52,17 @@ class TestCase(unittest.TestCase): this class mainly overriddes many of the common assert methods so to give a default message which includes the values - as well as the class-specific message_prefix string. + as well as the class-specific case_prefix string. this latter bit makes the output of various test cases easier to distinguish from eachother. """ - message_prefix = None + case_prefix = None def __init__(self, *a, **k): - #set the doc strings for all test messages to begin w/ message_prefix + #set the doc strings for all test messages to begin w/ case_prefix #yes, this is incredibly hacked. - prefix = self.message_prefix + prefix = self.case_prefix if prefix: if callable(prefix): prefix = prefix() @@ -120,6 +129,7 @@ class TestCase(unittest.TestCase): and remaining args and kwds are passed to function. """ for elem in cases: + elem = Params.norm(elem) correct = elem.args[0] result = func(*elem.args[1:], **elem.kwds) self.assertEqual(result, correct, diff --git a/passlib/util.py b/passlib/util.py index c8ea09e..492a851 100644 --- a/passlib/util.py +++ b/passlib/util.py @@ -270,5 +270,171 @@ def weighted_choice(rng, source): raise RuntimeError, "failed to sum weights correctly" #================================================================================= +# "hash64" encoding +#================================================================================= + +class H64: + """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 H64 class. + """ + + #string of all h64 chars; position in string corresponds to value encoded + CHARS = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + + @classmethod + def randstr(cls, count, rng=srandom): + "generate random hash64 string containing specified # of chars; usually used as salt" + CHARS = cls.CHARS + return ''.join(rng.choice(CHARS) for idx in xrange(count)) + + @classmethod + def encode_3_offsets(cls, 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 + CHARS = cls.CHARS + v1 = ord(buffer[o1]) + v2 = ord(buffer[o2]) + v3 = ord(buffer[o3]) + return CHARS[v1&0x3F] + \ + CHARS[((v2&0x0F)<<2) + (v1>>6)] + \ + CHARS[((v3&0x03)<<4) + (v2>>4)] + \ + CHARS[v3>>2] + + @classmethod + def encode_2_offsets(cls, buffer, o1, o2): + "do hash64 encode of two bytes at specified offsets in buffer; 2 missing msg set null; returns 3 chars" + CHARS = cls.CHARS + v1 = ord(buffer[o1]) + v2 = ord(buffer[o2]) + return CHARS[v1&0x3F] + \ + CHARS[((v2&0x0F)<<2) + (v1>>6)] + \ + CHARS[(v2>>4)] + + @classmethod + def encode_1_offset(cls, buffer, o1): + "do hash64 encode of single byte at specified offset in buffer; 4 missing msb set null; returns 2 chars" + CHARS = cls.CHARS + v1 = ord(buffer[o1]) + return CHARS[v1&0x3F] + CHARS[v1>>6] + + #old code, never used... + ###reverse map of char -> value + ##CHARIDX = dict( (c,i) for i,c in enumerate(CHARS)) + ##def _enc64(value, offset=0, num=False): + ## if num: + ## x, y, z = value[offset], value[offset+1], value[offset+2] + ## else: + ## x, y, z = ord(value[offset]), ord(value[offset+1]), ord(value[offset+2]) + ## #xxxxxx xxyyyy yyyyzz zzzzzz + ## #aaaaaa bbbbbb cccccc dddddd + ## a = (x >> 2) # x [8..3] + ## b = ((x & 0x3) << 4) + (y>>4) # x[2..1] + y [8..5] + ## c = ((y & 0xf) << 2) + (z>>6) #y[4..1] + d[8..7] + ## d = z & 0x3f + ## return CHARS[a] + CHARS[b] + CHARS[c] + CHARS[d] + ## + ##def _dec64(value, offset=0, num=False): + ## a, b, c, d = CHARIDX[value[offset]], CHARIDX[value[offset+1]], \ + ## CHARIDX[value[offset+2]], CHARIDX[value[offset+3]] + ## #aaaaaabb bbbbcccc ccdddddd + ## #xxxxxxxx yyyyyyyy zzzzzzzz + ## x = (a<<2) + (b >> 4) #a[6..1] + b[6..5] + ## y = ((b & 0xf) << 4) + (c >> 2) #b[4..1] + c[6..3] + ## z = ((c & 0x3) << 6) + d #c[2..1] + d[6..1] + ## if num: + ## return x, y, z + ## return chr(x) + chr(y) + chr(z) + ## + ##def h64_encode(value, pad=False, num=False): + ## "encode string of bytes into hash64 format" + ## if num: + ## value = list(value) + ## #pad value to align w/ 3 byte chunks + ## x = len(value) % 3 + ## if x == 2: + ## if num: + ## value += [0] + ## else: + ## value += "\x00" + ## p = 1 + ## elif x == 1: + ## if num: + ## value += [0, 0] + ## else: + ## value += "\x00\x00" + ## p = 2 + ## else: + ## p = 0 + ## assert len(value) % 3 == 0 + ## out = "".join( _enc64(value, offset, num=num) for offset in xrange(0, len(value), 3)) + ## assert len(out) % 4 == 0 + ## if p: + ## if pad: + ## out = out[:-p] + "=" * p + ## else: + ## out = out[:-p] + ## return out + ## + ##def h64_decode(value, pad=False, num=False): + ## "decode string of bytes from hash64 format" + ## if value.endswith("="): + ## assert len(value) % 4 == 0, value + ## if value.endswith('=='): + ## p = 2 + ## value = value[:-2] + '..' + ## else: + ## p = 1 + ## value = value[:-1] + '.' + ## else: + ## #else add padding if needed + ## x = len(value) % 4 + ## if x == 0: + ## p = 0 + ## elif pad: + ## raise ValueError, "size must be multiple of 4" + ## elif x == 3: + ## p = 1 + ## value += "." + ## elif x == 2: + ## p = 2 + ## value += ".." + ## elif x == 1: + ## p = 3 + ## value += "..." + ## assert len(value) % 4 == 0, value + ## if num: + ## out = [] + ## for offset in xrange(0, len(value), 4): + ## out.extend(_dec64(value, offset, num=True)) + ## else: + ## out = "".join( _dec64(value, offset) for offset in xrange(0, len(value), 4)) + ## assert len(out) % 3 == 0 + ## if p: #strip out garbage chars + ## out = out[:-p] + ## return out + +#================================================================================= #eof #================================================================================= |